该文章是直接翻译国外一篇文章,关于JS函数调用和"this"的处理。
都是基于原文处理的,其他的都是直接进行翻译可能有些生硬,所以为了行文方便,就做了一些简单的本地化处理。
如果想直接根据原文学习,可以忽略此文。
关于JS函数是如何调用的困惑了很多年,尤其是在JS函数中this
的语法机制很让人头疼。
在我看来,如果理解核心函数的调用机制,同时验证一些以核心函数为基础的其他实现方式的运行机制,关于上述所说的问题就会迎刃而解。
核心机制(The Core Primitive)
首先,让我们来解析一些核心函数的调用机制的重点---Function
对象的call
方法。 call
函数的调用过程如下:
- 将
parameters
第二个值到最后一个剥离出来并重新构建一个新的参数列表(argList
) - 传入函数的第一个值赋值给
thisValue
- 调用函数,在此过程中,将
thisValue
赋值给this
,argList
作为函数的参数列表(argument list)
示例如下:
function hello(thing){ console.log(this+ "says hello"+ thing); } hello.call("北宸南蓁","world"); //输出结果:北宸南蓁 says hello world 复制代码
正如实践之后所得到的结果,我们调用hello()
的时候,将this
的值赋值为北宸南蓁
同时将world
作为hello
运行时的参数list。上述的处理流程就是JS函数调用的核心机制。你可以这样粗略的认为:其他函数的调用机制就是在核心机制的基础上进行了封装/简化处理(desugar)。
简单函数调用(Simple Function Invocation)
很显然,利用call
调用函数看起来,不是一个很聪明的亚子。所以,JS运行利用简单语法 hello("world")
来直接调用函数。
function hello(thing){ console.log("Hello" + thing); } //简化的语法(封装之后的语法) hello("world") //核心语法 hello.call(window,"world"); 复制代码
NOTE:在ECMAScript 5的严格模式下有些许的不同:
//简化语法 hello("world"); //核心语法 hello(undefined,"world"); 复制代码
综上所述:可以将简单函数fn(...args)
的调用汇总为fn.call(window [ES5-strict:undefined],...args)
。
NOTE
上述的调用公式同样也适应于:
(funciton(){})()
==>(function(){}).call(window [ES5-strict:undefined])
成员函数(Member Functions)
在js的应用场景中,函数作为对象的属性也是很常见的情景。在这种情景下,会发生如下的简化处理:
var person ={ name:"北宸南蓁", hello:function(){ console.log(this + "says hello" + thing); } } //简化的语法 person.hello("world"); //核心语法/ person.hello.call(person,"world"); 复制代码
Note: 上述的简化过程不受成员函数的赋值和定义方式的影响的。例如上面的例子中,成员函数是直接定义在对象中。如果动态的对成员函数进行赋值,最后的简化结果也是一样的。
function hello(thing){ console.log(this + "says hello" + thing); } person = { name:"北宸南蓁"}; person.hello= hello; //简化语法 person.hello("world") //该种的核心语法也是 `person.hello.call(person,'world')` hello("world") //"[object DOMWindow]world" 复制代码
Note:上述所有的函数中this
的值都不是确定的。this
的值由调用函数的所在的 作用域决定。
用Function.prototype.bind
对作用域进行绑定
在某些应用场景中,需要将函数中的this
值进行绑定到指定的环境中。就需要额外的借助一个函数进行特定环境的绑定。
var person ={ name:"北宸南蓁", hello:function(thing){ console.log(this + "says hello " + thing); } } var boundHello = function(thing){ return person.hello.call(person,thing); } boundHello("world"); 复制代码
虽然在boundHello("world")
调用的时候,被脱糖(desugar)为boundHello.call(window,"world")
。但是在函数中,是将person对象的成员函数hello
进行this
值的处理,指向hello
函数应该在的作用域中。
或者我们可以将boundXX
函数变得更加通用。
var bind = function(func,thisValue){ return function(){ return func.apply(thisValue,arguments); } } var boundHello = bind(person.hello,person); boundHello("world"); 复制代码
其中实现的原理这里就不再赘述了。
由于这种场景很多,ES5在Function
对象中新增了bind
方法,用于对一个函数指定特定的this
值。
var boundHello = person.hello.bind(person); boundHello("world"); 复制代码
这种处理方式很有用,比如,你将一个成员函数作为callback
:
var person = { name: "北宸南蓁", hello: function() { console.log(this.name + " says hello world"); } } $("#some-div").click(person.hello.bind(person)); 复制代码
拨开云雾见月明
在上文中,为了能够在现有的规范语法中解释清楚函数调用的核心机制。通过func.call
来阐述函数底层是如何实现一系列的数据操作的。实际上,实现函数调用的核心语法另有其人[[Call]]
(这是一个内部属性),但是他是func.call
、[obj.]func()
实现的基础零件。
我们来了解一下func.call
的定义
- 如果
func
不是一个函数,直接抛出错误。 - 定义一个长度为0的
argList
- 如果传入函数的参数大于1个,从第一个参数
arg1
到参数结尾的所有参数的值作为一个新值,赋值给argList
. - Return the result of calling the
[[Call]]
internal method of func, providing thisArg as the this value and argList as the list of arguments.(这个话真的不好翻译,感觉还是原文的语句更加贴切)
正如上面的定义所知,func.call
的内部实现,都是基于[[Call]]
的操作来实现。也就是说,函数调用的核心就是**[[Call]]**的实现。但是这个方法的实现方式和func.call
的处理过程是一样的。所以,通过类比来模拟出函数的调用过程。