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)

callapply 这两个方法都在函数上调用。它们是通常的函数调用不一样,因为可以指定 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.prototypeArray.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)

这种方式很流行。相对于更长那种方式,短的那种方式并没有很清楚地反映代码书写者的意图,但是没那么啰嗦。执行速度上面,两种版本都没有太大区别。

results matching ""

    No results matching ""