深入理解ES6-函数和对象扩展

本篇整理了《深入理解ES6》书中的函数和对象扩展章节中的重要知识点。

函数

函数形参的默认值

ES6语法

1
function f(url, timeout = 2000){}

默认参数值对arguments对象的影响

  1. ES5非严格模式,函数命名参数的变化会体现在arguments对象上。
  2. ES5严格模式,取消了该行为,无论参数怎么变化,arguments不再随之改变。
  3. ES6中,如果使用了默认参数值,则无论是否显示定义严格模式,arguments对象的行为都将与ES5严格模式下保持一致。

默认参数表达式

默认参数是在函数调用时求值。

1
2
3
function f(url, timeout = getDefaultValue()){}
function m(first, second = first){}
function n(first, second = getValue(first)){}

在引用默认值的时候,只允许引用前面的参数值,即先定义的参数不能访问后定义的参数。

1
function m(first = second, second){} // 抛出错误

处理无命名参数

不定参数

  1. 在参数前加...,表示这是一个不定参数。
  2. 每个函数最多可以声明一个不定参数,而且必须放在所有参数末尾。
  3. 不定参数不能用于对象字面量setter之中。

增强的Function构造函数

支持在创建函数时定义默认参数和不定参数。

展开运算符

与不定参最相似的是展开运算符。不定参数将多个参数整合为一个数组,而展开运算符刚好相反,可以将一个数组打散作为各自独立的参数传入函数。

1
2
3
4
5
const values = [2, 3, 4];
console.log(Math.max(...values)); // 4
const vals = [2, 3, 4];
console.log(Math.max.apply(Math, vals)); // 4

name属性

ES6中所有函数的name属性都有一个合适的值。

1
2
3
4
5
6
7
8
function doSomething(){} // "doSomething"
doSomething.bind() // "bound doSomething"
var doAnotherThing = function(){} // "doAnotherThing"
var person = {
get firstName(){}, // "get firstName"
sayName: function(){} // "sayName"
};
new Function() // "anonymous"

明确函数的多重用途

ES5及早期版本中的函数具有多重功能,可以结合new使用,函数内的this 将指向一个新对象,函数最终会返回这个新对象。

ES6中在js函数中定义了两个不同的内部方法:[[Call]]和[[Construct]]。当通过 new 关键字调用函数时,执行的是[[Construct]]函数,它负责创建一个被称作实例的新对象,然后再执行函数体,并将this绑定到该实例上;如果不是通过 new 关键字调用函数,则执行[[Call]]函数,从而直接执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。切记,并不是所有函数都有[[Construct]]方法,例如箭头函数。

ES5中判断函数被调用的方法

想确定一个函数是否通过 new 关键字被调用,最流行的方法是使用 instanceof。

1
2
3
4
5
6
7
function Person(name){
if(this instanceof Person){
this.name = name;
}else{
throw new Error("必须通过new关键字来调用");
}
}

通常来讲是没什么大问题的,但是并不可靠。可以通过Person.call()调用成功。

原属性 new.target

为了解决是否通过 new 关键字调用的问题,ES6引入了new.target这个元属性。 元属性是指非对象的属性,其可以提供非对象目标的补充信息,如new。当调用函数的[[Construct]]方法时,new.target被赋值为new操作符的目标,通常是新创建的实例;如果调用[[Call]],new.target为undefined。所以可以通过检测new.target来确认是否通过new 关键字来调用的函数。

1
2
3
4
5
6
7
function Person(name){
if(typeof new.target !== "undefined"){
this.name = name;
}else{
throw new Error("必须通过new关键字来调用");
}
}

块级函数

  1. ES5严格模式下在代码块内部声明函数会报错;
  2. ES6代码块定义的函数视为一个块级声明,从而可以定一个该函数的代码块内访问和调用。块级函数会提升至顶部(严格模式下),在非严格模式下,不是提升至代码块顶部,而是提升至外围函数或全局作用域的顶部。

箭头函数

它与传统函数有些许不同,如下:

  1. 没有this、super、arguments和new.target绑定。
  2. 不能通过new 关键字调用。
  3. 没有原型。
  4. 不可以改变this的绑定。
  5. 不支持arguments对象。
  6. 不支持重复的命名参数

常用的一些写法:

1
2
3
4
5
6
7
8
let reflect = value => value;
let sum = (num1, num2) => num1 + num2;
let getName = () => "zhangsan";
let sum = (num1, num2) => {
return num1 + num2;
};
let doNothing = () => {};
let getPerson = id => ({id: id, name: "zhangsan"});

尾部调用优化

在ES5的引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的栈帧都会保存在内存中。

ES6尾调用优化

如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧。

  1. 尾调用不访问当前栈帧变量。
  2. 在函数内部,尾调用时最后一条语句。
  3. 尾调用的结果作为函数值返回。

扩展对象的功能性

对象字面量语法扩展

属性初始值的简写

当一个对象的属性与本地变量同名时,不必再写冒号和值,只需要只写属性名即可。

对象方法的简写

可以省略冒号和function关键字。

可计算属性名

使用方括号表示的属性名是可计算的,它的内容将被求值并被最终转化为一个字符串。

新增方法

Object.is()

该方法用来弥补全等 === 运算符的不准确,举个例子 +0 和 -0在Javascript引擎中表示为两个完全不同的实体,而如果使用 === 比较得到的是 true;同样, NaN === NaN 的返回值为false,需要使用isNaN()方法才可以正确检测NaN。

1
2
3
4
5
6
7
+0 == -0; // true
+0 === -0; // true
Object.is(+0, -0); // false
NaN == NaN; // false
NaN === NaN; // false
Object.is(NaN, NaN); // true

Object.assign()

该方法可以接受任意数量的源对象,第一个参数为接收对象,会将其它对象复制到接收对象上。不能复制提供者的访问器属性。

重复的对象字面量属性

ES5严格模式加入了字面量重复属性的校验,当同时存在多个同名属性是会抛出错误。而在ES6中重复属性检查被移除了,不管是不是严格模式,重复属性出现时,只保留最后一个。

自有属性枚举顺序

ES5中未定义对象属性的枚举顺序,由Javascript引擎厂商自行解决。ES6严格规定了对象的自有属性被枚举时的返回属性,这会影响到Object.getOwnPropertyName()方法及Reflect.ownKeys返回属性的方式,Object.assign()方法处理属性的顺序也将随之改变。

自有属性枚举顺序的基本规则是:

  1. 所有数字键按升序排序;
  2. 所有字符串按照它们被加入对象的顺序排序;
  3. 所有symbol键按照它们被加入对象的顺序排序。

增强对象原型

改变对象的原型

ES6添加了Object.setPrototypeOf()方法来改变对象的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let person = {
getGreeting(){
return "Hello";
}
};
let dog = {
getGreeting(){
return "Woof";
}
};
// 以person对象为原型
let friend = Object.create(person);
console.log(friend.getGreeting()); // "Hello"
console.log(Object.getPrototypeOf(friend) === person) // true
// 将原型设置为dog
Object.setPrototypeOf(friend, dog)
console.log(friend.getGreeting()); // "Woof"
console.log(Object.getPrototypeOf(friend) === dog) // true

简化原型访问的Super引用

ES6引入了Super引用的特性,使用它可以更便捷地访问对象原型。举个例子,如果你想重写对象实例的方法,又需要调用与它同名的原型方法,在ES5中可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let person = {
getGreeting(){
return "Hello";
}
};
let dog = {
getGreeting(){
return "Woof";
}
};
let friend = {
getGreeting(){
return Object.getPrototypeOf(this).getGreenting.call(this) + ", hi!";
}
};
// 以person对象为原型
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); // "Hello, hi!"
console.log(Object.getPrototypeOf(friend) === person) // true
// 将原型设置为dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // "Woof, hi!"
console.log(Object.getPrototypeOf(friend) === dog) // true

通过Object.getPrototypeOf()方法和call来实现调用原型上的方法,略显复杂。ES6引入super来解决该问题,Super引用相当于指向对象原型的指针,实际上也就是Object.getPrototypeOf(this)的值。于是,可以简化上面getGreeting方法:

1
2
3
4
5
let friend = {
getGreeting(){
return super.getGreenting() + ", hi!";
}
};

正式的方法定义

ES6中正式将方法定义为一个函数,它会有一个内部的[[HomeObject]]属性来容纳这个方法从属的对象。请思考以下代码:

1
2
3
4
5
6
7
8
9
10
11
let person = {
// 是方法
getGreeting(){
return "Hello";
}
};
// 不是方法
function share(){
return "hi";
}

getGreeting()方法的[[HomeObject]]属性值为person。share没有明确定义[[HomeObject]]属性。

Super的所有引用都通过[[HomeObject]]属性来确定后续的运行过程。第一步是在[[HomeObject]]属性上调用Object.getPrototypeOf()方法来检索原型的引用;然后搜索原型找到同名函数;最后,设置this绑定并且调用相应的方法。

上面例子friend.getGreeting()方法的[[HomeObject]]为friend,而friend的原型为person,所以super.getGreeting()等价于person.getGreeting.call(this);