理解JS函数调用和"this"

1,014 阅读5分钟

该文章是直接翻译国外一篇文章,关于JS函数调用和"this"的处理。
都是基于原文处理的,其他的都是直接进行翻译可能有些生硬,所以为了行文方便,就做了一些简单的本地化处理。
如果想直接根据原文学习,可以忽略此文。

关于JS函数是如何调用的困惑了很多年,尤其是在JS函数中this的语法机制很让人头疼。

在我看来,如果理解核心函数的调用机制,同时验证一些以核心函数为基础的其他实现方式的运行机制,关于上述所说的问题就会迎刃而解。

核心机制(The Core Primitive)

首先,让我们来解析一些核心函数的调用机制的重点---Function对象的call方法。 call函数的调用过程如下:

  1. parameters第二个值到最后一个剥离出来并重新构建一个新的参数列表(argList)
  2. 传入函数的第一个值赋值给thisValue
  3. 调用函数,在此过程中,将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的定义

  1. 如果func不是一个函数,直接抛出错误。
  2. 定义一个长度为0的argList
  3. 如果传入函数的参数大于1个,从第一个参数arg1到参数结尾的所有参数的值作为一个新值,赋值argList.
  4. 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的处理过程是一样的。所以,通过类比来模拟出函数的调用过程。