5 函数的扩展
5.1 参数默认值
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
1 | function log(x, y) { |
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
1 | function log(x, y = 'World') { |
5.2 结合解构赋值
参数默认值可以与解构赋值的默认值,结合起来使用。
1 | function foo({x, y = 5}) { |
双重默认值
函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。1
2
3
4
5
6function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
5.3 函数的length属性
length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数
1 | (function (a) {}).length // 1 |
5.4 作用域
如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的。
即先是当前函数的作用域,然后才是全局作用域。
1 | var x = 1; |
1 | let x = 1; |
函数调用时,y的默认值变量x尚未在函数内部生成,所以x指向全局变量。
1 | let x = 1; |
函数foo的默认值x的作用域是函数作用域,而不是全局作用域。
由于在函数作用域中,存在变量x,但是默认值在x赋值之前先执行了,所以这时属于暂时性死区,任何对x的操作都会报错。
1 | var x = 1; |
函数foo的参数y的默认值是一个匿名函数。函数foo调用时,它的参数x的值为undefined,所以y函数内部的x一开始是undefined,后来被重新赋值2。
但是,函数foo内部重新声明了一个x,值为3,这两个x是不一样的,互相不产生影响,因此最后输出3。
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
1 | function throwIfMissing() { |
5.5 rest参数
ES6引入rest参数(形式为“…变量名”),用于获取函数的多余参数。
1 | function add(...values) { |
rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
5.6 扩展运算符
扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
1 | console.log(...[1, 2, 3]) |
1. 合并数组
1 | // ES5 |
2. 与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
1 | // ES5 |
1 | const [first, ...rest] = [1, 2, 3, 4, 5]; |
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
5.6 name属性
函数的name属性,返回该函数的函数名。
1 | function foo() {} |
如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而ES6的name属性会返回实际的函数名。
1 | var func1 = function () {}; |
如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数原本的名字。
1 | const bar = function baz() {}; |
5.7 箭头函数
1 | // ES5 |
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。
this对象的指向是可变的,但是在箭头函数中,它是固定的。
1 | function foo() { |
上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。
如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。
但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。
1 | function Timer() { |
上面代码中,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 | foo::bar; |
6 对象的扩展
6.1 属性简写
ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
1 | var foo = 'bar'; |
6.2 方法简写
除了属性简写,方法也可以简写。
1 | var o = { |
下面是一个实际的例子。
1 | var birth = '2000/01/01'; |
CommonJS模块输出变量,就非常合适使用简洁写法。
1 | var ms = {}; |
如果某个方法的值是一个Generator函数,前面需要加上星号。
1 | var obj = { |
6.3 定义对象
ES6 允许字面量定义对象
1 | let propKey = 'foo'; |
表达式还可以用于定义方法名。
1 | let obj = { |
6.4 对象新方法
1. Object.is()
ES5比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。
它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
1 | Object.is('foo', 'foo') |
2. Object.assign()
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
1 | var target = { a: 1 }; |
如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
注意点
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
1 | var obj1 = {a: {b: 1}}; |
上面代码中,源对象obj1的a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
(1)为对象添加属性
1 | class Point { |
(2)为对象添加方法
1 | Object.assign(SomeClass.prototype, { |
(3)克隆对象
1 | function clone(origin) { |
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
1 | function clone(origin) { |
(4)合并多个对象
1 | const merge = |
6.5 原型属性
1. proto属性
用来读取或设置当前对象的prototype对象。
1 | // es6的写法 |
2. Object.setPrototypeOf()
Object.setPrototypeOf方法的作用与proto相同,用来设置一个对象的prototype对象。
它是ES6正式推荐的设置原型对象的方法。
1 | let proto = {}; |
上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。
3. Object.getPrototypeOf()
该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。
1 | function Rectangle() { |
6.6 Object一些方法
1. Object.keys()
ES5引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
1 | var obj = { foo: "bar", baz: 42 }; |
2. Object.values()
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。1
2
3var obj = { foo: "bar", baz: 42 };
Object.values(obj)
// ["bar", 42]
3. Object.entries
Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
1 | var obj = { foo: 'bar', baz: 42 }; |
除了返回值不一样,该方法的行为与Object.values基本一致。
6.6 对象的扩展运算符
1. 解构赋值
解构赋值要求等号右边是一个对象
解构赋值必须是最后一个参数,否则会报错。
1 | let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; |
2. 扩展运算符
1 | let z = { a: 3, b: 4 }; |
6.7 对象属性的描述对象
ES5有一个Object.getOwnPropertyDescriptor方法,返回某个对象属性的描述对象(descriptor)。
1 | var obj = { p: 'a' }; |
ES7有一个提案,提出了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。
1 | const obj = { |
7 symbol
使用了一个他人提供的对象,想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的,这样就从根本上防止了属性名冲突。这就是ES6引入Symbol的原因
它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
1 | let s = Symbol(); |
Symbol函数前不能使用new命令,否则会报错。
Symbol值不是对象,所以不能添加属性
如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。
1 | let firstName = Symbol(); |
7.1 参数区分
Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
1 | var s1 = Symbol('foo'); |
上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。
Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
1 | // 没有参数的情况 |
7.2 类型转换
Symbol值不能与其他类型的值进行运算,会报错。
1 | var sym = Symbol('My symbol'); |
但是,Symbol值可以显式转为字符串。
1 | var sym = Symbol('My symbol'); |
另外,Symbol值也可以转为布尔值,但是不能转为数值。
1 | var sym = Symbol(); |
7.3 作为属性名
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性
1 | var mySymbol = Symbol(); |
Symbol值作为对象属性名时,不能用点运算符。
1 | var mySymbol = Symbol(); |
上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个Symbol值。
在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中
7.4 Symbol.for()
Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
1 | Symbol.for("bar") === Symbol.for("bar") |
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
1 | var s1 = Symbol.for("foo"); |
最后更新: 2019年02月24日 16:44