12.3 ECMAScript 5 和 6 中的传递方法调用消息和直接调用方法
在 JavaScript 中有两种方式调用方法:
- 通过发送调用消息,例如
obj.someMethod(arg0, arg1)
- 直接地,比如
someFunc.call(thisValue, arg0, arg1)
本节解释了这两种方式如何运作,和为什么在 ES6 中将会很少直接调用方法。在开始之前,先复习一下关于原型链的知识。
12.3.1 背景知识:原型链
记住 JavaScript 中每一个对象实际上都是在一条链上的,这条链上面有一个或多个对象。第一个对象从后一个对象上继承属性。例如,数组 ['a', 'b']
的原型链看起来像这样:
- 1、存放元素
'a'
和'b'
的实例 - 2、
Array
构造器提供的属性集Array.prototype
- 3、
Object
构造器提供的属性集Object.prototype
- 4、
null
(链的末端,因此不是一个真正的成员)
可以通过 Object.getPrototypeOf()
查看原型链:
> var arr = ['a', 'b'];
> var p = Object.getPrototypeOf;
> p(arr) === Array.prototype
true
> p(p(arr)) === Object.prototype
true
> p(p(p(arr)))
null
“前面的”对象上的属性覆盖“后面”对象上的属性。例如, Array.prototype
提供了一个数组版本的 toString()
方法,覆盖了 Object.prototype.toString()
。
> var arr = ['a', 'b'];
> Object.getOwnPropertyNames(Array.prototype)
[ 'toString', 'join', 'pop', ··· ]
> arr.toString()
'a,b'
12.3.2 发送方法调用消息
方法调用 arr.toString()
实际上包含两个步骤:
- 1、消息发送:在
arr
的原型链上,找到第一个名字为toString
的属性。 - 2、调用:调用找到的值,然后设置隐式的
this
参数为消息接收者arr
。
你可以通过使用函数的 call()
方法显示地执行这两个步骤:
> var func = arr.toString; // dispatch
> func.call(arr) // direct call, providing a value for `this`
'a,b'
12.3.3 直接方法调用
在 JavaScript 中有两种方式直接调用方法:
Function.prototype.call(thisValue, arg0?, arg1?, ···)
Function.prototype.apply(thisValue, argArray)
call
和 apply
这两个方法都在函数上调用。它们是通常的函数调用不一样,因为可以指定 this
值。 call
在方法调用中一个个地提供参数, apply
是通过一个数组提供。
通过动态发送消息的方式调用方法有一个问题,方法需要在对象的原型链中。 call()
使得可以在调用一个方法的时候直接指定消息接收者。这意味着可以其它对象中借出一个不在当前原型链中的方法。例如,可以对 arr
调用原始的未被覆盖的 Object.prototype.toString
方法:
> Object.prototype.toString.call(arr)
'[object Array]'
对大量对象都有效的方法(不仅仅是“它们的”构造器生成的实例)称作通用方法( generic )。《 Speaking JavaScript 》中有一组方法全是通用方法,包含大多数数组方法和所有 Object.prototype
(这上面的方法必须要能正确处理所有对象,因此隐式地就是通用方法了)上的方法。
12.3.4 直接方法调用的使用场景
本节涵盖了直接方法调用的使用场景。每一次,我会先描述 ES5 中的使用场景,然后是 ES6 中的变化(将会很少使用直接方法调用了)。
12.3.4.1 ES5 :通过一个数组给一个方法提供参数
一些函数接收多个值,但是每个参数对应一个值。如果想通过一个数组传入这些值,怎么做?
例如, push()
给一个数组追加几个值:
> var arr = ['a', 'b'];
> arr.push('c', 'd')
4
> arr
[ 'a', 'b', 'c', 'd' ]
但是却不能追加整个数组。可以通过使用 apply()
来变通处理这个限制:
> var arr = ['a', 'b'];
> Array.prototype.push.apply(arr, ['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]
类似地, Math.max()
和 Math.min()
仅对单一值有效:
> Math.max(-1, 7, 2)
7
有了 apply()
,可以用数组的方式来使用这些方法:
> Math.max.apply(null, [-1, 7, 2])
7
12.3.4.2 ES6 :扩展操作符(...)基本上取代了 apply()
通过 apply()
的直接的方法调用,仅仅是因为像把一个数组转换成一组参数显得很笨拙,这就是为什么 ECMAScript 6 有扩展操作符(...)。甚至在发送方法调用消息中也可以使用。
> Math.max(...[-1, 7, 2])
7
另一个例子:
> let arr = ['a', 'b'];
> arr.push(...['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]
扩展操作符对 new
操作符同样有效:
> new Date(...[2011, 11, 24])
Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
注意 apply()
不能和 new
结合使用 - 上述功能在 ECMAScript 5 中只能通过复杂的迂回方式来实现。
12.3.4.3 ES5:把类数组对象转换成数组
JavaScript 中的一些对象是类数组的,它们很像数组了,但是没有任何数组方法。看两个例子。
第一个,函数中特殊的变量 arguments
就是类数组的。它有一个 length
属性和索引化的元素访问。
> var args = function () { return arguments }('a', 'b');
> args.length
2
> args[0]
'a'
但是 arguments
并不是 Array
的实例,没有 forEach()
方法。
> args instanceof Array
false
> args.forEach
undefined
第二个, DOM 方法 document.querySelectorAll()
返回一个 NodeList
的实例。
> document.querySelectorAll('a[href]') instanceof NodeList
true
> document.querySelectorAll('a[href]').forEach // no Array methods!
undefined
对于很多复杂的操作,需要先将类数组对象转换成数组。这通过 Array.prototype.slice()
实现。该方法将方法调用接收者的元素拷贝到一个新的数组里面:
> var arr = ['a', 'b'];
> arr.slice()
[ 'a', 'b' ]
> arr.slice() === arr
false
如果直接调用 slice()
,可以将一个 NodeList
转换成数组:
var domLinks = document.querySelectorAll('a[href]');
var links = Array.prototype.slice.call(domLinks);
links.forEach(function (link) {
console.log(link);
});
也可以将 arguments
转换成数组:
function format(pattern) {
// params start at arguments[1], skipping `pattern`
var params = Array.prototype.slice.call(arguments, 1);
return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
12.3.4.4 ES6:类数组对象不再恼人
一方面, ECMAScript 6 有 Array.from()
,一个简单地将类数组对象转换成数组的方法:
let domLinks = document.querySelectorAll('a[href]');
let links = Array.from(domLinks);
links.forEach(function (link) {
console.log(link);
});
另一方面,不会再需要类数组的 arguments
,因为 ECMAScript 6 有剩余参数(通过三个点声明):
function format(pattern, ...params) {
return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
12.3.4.5 ES5 :安全地使用 hasOwnProperty()
obj.hasOwnProperty('prop')
可以辨别 obj
是否有自有(不是继承来的)属性 prop
。
> var obj = { prop: 123 };
> obj.hasOwnProperty('prop')
true
> 'toString' in obj // inherited
true
> obj.hasOwnProperty('toString') // own
false
然而,如果覆盖了 Object.prototype.hasOwnProperty
,那么通过发送方法调用消息的方式调用 hasOwnProperty
将会得到错误的结果。
> var obj1 = { hasOwnProperty: 123 };
> obj1.hasOwnProperty('toString')
TypeError: Property 'hasOwnProperty' is not a function
如果 Object.prototype
没有在对象的原型链上, hasOwnProperty
就不能通过发送消息的方式调用了。
> var obj2 = Object.create(null);
> obj2.hasOwnProperty('toString')
TypeError: Object has no method 'hasOwnProperty'
在这两种情形下,解决的方法是使用直接调用的方式:
> var obj1 = { hasOwnProperty: 123 };
> Object.prototype.hasOwnProperty.call(obj1, 'hasOwnProperty')
true
> var obj2 = Object.create(null);
> Object.prototype.hasOwnProperty.call(obj2, 'toString')
false
12.3.4.6 ES6 :很少使用 hasOwnProperty()
hasOwnProperty()
大多数时候用于通过对象实现 Map 。所幸的是, ECMAScript 6 有内置的 Map
数据结构,这意味着将会很少使用 hasOwnProperty()
。
12.3.4.7 ES5 :避免中间对象
在字符串上调用数组特有的方法(比如 join()
)一般都包含两个步骤:
var str = 'abc';
var arr = str.split(''); // step 1
var joined = arr.join('-'); // step 2
console.log(joined); // a-b-c
字符串是类数组的,可以成为通用数组方法的 this
值。因此,直接调用的话就可以少掉第一步:
var str = 'abc';
var joined = Array.prototype.join.call(str, '-');
类似地,在拆分( split )了字符串之后或者通过直接调用,都可以在字符串上调用 map()
:
> function toUpper(x) { return x.toUpperCase() }
> 'abc'.split('').map(toUpper)
[ 'A', 'B', 'C' ]
> Array.prototype.map.call('abc', toUpper)
[ 'A', 'B', 'C' ]
注意直接的调用可能更加高效,也更优雅,这种方式是很值得的!
12.3.4.8 ES6 :避免中间对象
如果第二个参数传入回调函数, Array.from()
可以在一步之内执行转换和 map 操作。
> Array.from('abc', ch => ch.toUpperCase())
[ 'A', 'B', 'C' ]
提示一下,两步完成的方法是:
> 'abc'.split('').map(function (x) { return x.toUpperCase() })
[ 'A', 'B', 'C' ]
12.3.5 Object.prototype
和 Array.prototype
的缩写
你可以通过一个空的对象字面量(它的原型是 Object.prototype
)来访问 Object.prototype
上面的方法。例如,下面两个直接方法调用是等价的:
Object.prototype.hasOwnProperty.call(obj, 'propKey')
{}.hasOwnProperty.call(obj, 'propKey')
对于 Array.prototype
也是同样的:
Array.prototype.slice.call(arguments)
[].slice.call(arguments)
这种方式很流行。相对于更长那种方式,短的那种方式并没有很清楚地反映代码书写者的意图,但是没那么啰嗦。执行速度上面,两种版本都没有太大区别。