5 函数的扩展

5.1 参数默认值

在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

1
2
3
4
5
6
7
8
function log(x, y) {
y = y || 'World';
console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

1
2
3
4
5
6
7
function log(x, y = 'World') {
console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

5.2 结合解构赋值

参数默认值可以与解构赋值的默认值,结合起来使用。

1
2
3
4
5
6
7
8
function foo({x, y = 5}) {
console.log(x, y);
}

foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined

双重默认值
函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。

1
2
3
4
5
6
function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}

fetch('http://example.com')
// "GET"

5.3 函数的length属性

length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数

1
2
3
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

5.4 作用域

如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的。
即先是当前函数的作用域,然后才是全局作用域。

1
2
3
4
5
6
7
var x = 1;

function f(x, y = x) {
console.log(y);
}

f(2) // 2
1
2
3
4
5
6
7
8
9
let x = 1;

function f(y = x) {
let x = 2;
console.log(y);
console.log(x);
}

f() // 1,2

函数调用时,y的默认值变量x尚未在函数内部生成,所以x指向全局变量。

1
2
3
4
5
6
7
let x = 1;

function f(x = x) {
console.log(x);
}

f() // x is not defined

函数foo的默认值x的作用域是函数作用域,而不是全局作用域。
由于在函数作用域中,存在变量x,但是默认值在x赋值之前先执行了,所以这时属于暂时性死区,任何对x的操作都会报错。

1
2
3
4
5
6
7
8
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}

foo() // 3

函数foo的参数y的默认值是一个匿名函数。函数foo调用时,它的参数x的值为undefined,所以y函数内部的x一开始是undefined,后来被重新赋值2。
但是,函数foo内部重新声明了一个x,值为3,这两个x是不一样的,互相不产生影响,因此最后输出3。

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

1
2
3
4
5
6
7
8
9
10
function throwIfMissing() {
throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}

foo()
// Error: Missing parameter

5.5 rest参数

ES6引入rest参数(形式为“…变量名”),用于获取函数的多余参数。

1
2
3
4
5
6
7
8
9
10
11
function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10

rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

5.6 扩展运算符

扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

1. 合并数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];

// ES5的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

2. 与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

1
2
3
4
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
1
2
3
4
5
6
7
8
9
10
11
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest // []:

const [first, ...rest] = ["foo"];
first // "foo"
rest // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。


5.6 name属性

函数的name属性,返回该函数的函数名。

1
2
function foo() {}
foo.name // "foo"

如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而ES6的name属性会返回实际的函数名。

1
2
3
4
5
6
7
var func1 = function () {};

// ES5
func1.name // ""

// ES6
func1.name // "func1"

如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数原本的名字。

1
2
3
4
5
6
7
const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

5.7 箭头函数

1
2
3
4
5
6
// ES5
var f = function(v) {
return v;
};
// ES6
var f = v => v;

箭头函数有几个使用注意点。

  • (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  • (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  • (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

  • (4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

this对象的指向是可变的,但是在箭头函数中,它是固定的。

1
2
3
4
5
6
7
8
9
10
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。
如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。
但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。
前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。
所以,3100毫秒之后,timer.s1被更新了3次,而timer.s2一次都没更新(因为代码调用更新的不是timer的s2)。

5.8 绑定 this

ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。

函数绑定运算符是并排的两个双冒号(::),
双冒号左边是一个对象,右边是一个函数。
该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

1
2
3
4
5
6
7
foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

6 对象的扩展

6.1 属性简写

ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var foo = 'bar';
var baz = {foo};
baz // {foo: "bar"}

// 等同于
var baz = {foo: foo};

// 如果属性名和参数名相同,可以简写

function f(x, y) {
return {x, y};
}

// 等同于

function f(x, y) {
return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

6.2 方法简写

除了属性简写,方法也可以简写。

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = {
method() {
return "Hello!";
}
};

// 等同于

var o = {
method: function() {
return "Hello!";
}
};

下面是一个实际的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
var birth = '2000/01/01';

var Person = {

name: '张三',

//等同于birth: birth
birth,

// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }

};

CommonJS模块输出变量,就非常合适使用简洁写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ms = {};

function getItem (key) {
return key in ms ? ms[key] : null;
}

function setItem (key, value) {
ms[key] = value;
}

function clear () {
ms = {};
}

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};

如果某个方法的值是一个Generator函数,前面需要加上星号。

1
2
3
4
5
var obj = {
* m(){
yield 'hello world';
}
};

6.3 定义对象

ES6 允许字面量定义对象

1
2
3
4
5
6
let propKey = 'foo';

let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

表达式还可以用于定义方法名。

1
2
3
4
5
6
7
let obj = {
['h' + 'ello']() {
return 'hi';
}
};

obj.hello() // hi

6.4 对象新方法

1. Object.is()
ES5比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。
它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

1
2
3
4
5
6
7
8
9
10
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

2. Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

1
2
3
4
5
6
7
var target = { a: 1 };

var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

注意点

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

1
2
3
4
5
var obj1 = {a: {b: 1}};
var obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

(1)为对象添加属性

1
2
3
4
5
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}

(2)为对象添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});

// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
...
};
SomeClass.prototype.anotherMethod = function () {
...
};

(3)克隆对象

1
2
3
function clone(origin) {
return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

1
2
3
4
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}

(4)合并多个对象

1
2
const merge =
(...sources) => Object.assign({}, ...sources);

6.5 原型属性

1. proto属性

用来读取或设置当前对象的prototype对象。

1
2
3
4
5
6
7
8
9
// es6的写法
var obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es5的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

2. Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与proto相同,用来设置一个对象的prototype对象。
它是ES6正式推荐的设置原型对象的方法。

1
2
3
4
5
6
7
8
9
10
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

3. Object.getPrototypeOf()

该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。

1
2
3
4
5
6
7
8
9
10
11
function Rectangle() {
}

var rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

6.6 Object一些方法

1. Object.keys()

ES5引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

1
2
3
var obj = { foo: "bar", baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

2. Object.values()

Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

1
2
3
var obj = { foo: "bar", baz: 42 };
Object.values(obj)
// ["bar", 42]

3. Object.entries
Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

1
2
3
var obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]

除了返回值不一样,该方法的行为与Object.values基本一致。


6.6 对象的扩展运算符

1. 解构赋值

解构赋值要求等号右边是一个对象

解构赋值必须是最后一个参数,否则会报错。

1
2
3
4
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

2. 扩展运算符

1
2
3
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

6.7 对象属性的描述对象

ES5有一个Object.getOwnPropertyDescriptor方法,返回某个对象属性的描述对象(descriptor)。

1
2
3
4
5
6
7
8
var obj = { p: 'a' };

Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }

ES7有一个提案,提出了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
foo: 123,
get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: bar],
// set: undefined,
// enumerable: true,
// configurable: true } }

7 symbol

使用了一个他人提供的对象,想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的,这样就从根本上防止了属性名冲突。这就是ES6引入Symbol的原因

它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

1
2
3
let s = Symbol();

typeof s

Symbol函数前不能使用new命令,否则会报错。
Symbol值不是对象,所以不能添加属性

如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

1
2
3
4
5
6
7
8
9
10
let firstName = Symbol();
let person = {};
person[firstName] = "666";
person.firstName ="777";
console.log(person[firstName]); // 666
console.log(person.firstName); // 777
console.dir(person)
//fiestName:"666"
// Symbol(): "huochai"
//__proto__:Object

7.1 参数区分

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

1
2
3
4
5
6
7
8
var s1 = Symbol('foo');
var s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"

上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

1
2
3
4
5
6
7
8
9
10
11
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');

s1 === s2 // false

7.2 类型转换

Symbol值不能与其他类型的值进行运算,会报错。

1
2
3
4
5
6
var sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string

但是,Symbol值可以显式转为字符串。

1
2
3
4
var sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

另外,Symbol值也可以转为布尔值,但是不能转为数值。

1
2
3
4
5
6
7
8
9
10
var sym = Symbol();
Boolean(sym) // true
!sym // false

if (sym) {
// ...
}

Number(sym) // TypeError
sym + 2 // TypeError

7.3 作为属性名

由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

Symbol值作为对象属性名时,不能用点运算符。

1
2
3
4
5
6
var mySymbol = Symbol();
var a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个Symbol值。

在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中

7.4 Symbol.for()

Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

1
2
3
4
5
Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。

1
2
3
4
5
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

最后更新: 2019年02月24日 16:44

原始链接: http://linjiad.github.io/2019/02/23/ES6-2/

× 请我吃糖~
打赏二维码