简析JavaScript中的最复杂的机制之一——this

this是JavaScript中最复杂的机制之一,被自动定义在所有函数的作用域中。“When a function of an object was called , the object will be passed to the execution context as 'this' value.”当一个函数被调用时,拥有这个函数的对象会作为this传入,所以this可以是全局对象,当前对象乃至任何对象
例如:

function f(){
    var name = "Funny";
    console.log(this.name);  //undefined
    console.log(this);   //window
}
f();    //f();与window.f();效果相同

调用函数f,因为函数f是最外层的函数,所以它拥有全局作用域,换句话说,它是全局对象(window)的一个方法(全局变量是全局对象的属性或方法)。console.log(this.name),程序执行到这里时,会对全局作用域进行RHS查询名为name的变量,由于不存在,所以控制台输出‘undefined’,console.log(this),输出全局对象window。
 为什么要用this这个关键字?倘若我们不用this:

var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
function identify(context){
    return context.name.toUpperCase();
}

function speak(context){
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}
identify(you);  //READER
speak(me);  //Hello,I'm KYLE

如果不用this来传递上下文对象,而采用显式传递上下文对象,这会让代码变得越来越混乱,尤其当使用模式越来越复杂的时候(上面的例子代码特别简单)。函数自动引用合适的上下文对象十分重要:

var you = {
    name: "Reader";
};
var me = {
    name: "Kyle"
};
function identify(){
    return this.name.toUpperCase();
}
function speak(){
    var greeting = "Hello,I'm " + identify.call(this);
    console.log(greeting);
}
identify.call(you); //READER
speak.call(me); //Hello,I'm KYLE

this并非指向函数本身

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0;i < 10; i++){
    if(i > 5){
        foo(i); //foo: 6;foo: 7;foo: 8;foo: 9;
    }
}
console.log(foo.count);     //0

按道理,count是计算函数foo调用的次数,foo.count = 0的确为函数对象foo添加一个名为count的属性,但是foo函数内部的this.count中的this并非指向foo函数对象,而是指向window(全局对象)。在非严格模式下,this.count++;,会对count进行LHS查询,然而并没有在全局作用域中找到,所以就创建一个名为count的全局变量(严格模式下,程序会抛出引用异常);随着函数foo的调用,全局对象的属性count就递增,而函数对象的属性count则不变化,当然,console.log(count);会输出4。
如果要从函数对象内部引用它本身,那么只使用this是不够的。我们可以强制this指向foo函数本身:

function foo(num){
    console.log("foo: " + num);
    this.count++;
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
    if(i > 5){
        foo.call(foo,i);
    }
}
console.log(foo.count);     //4

大多数情况下this并非指向函数的作用域
this在任何情况下都不会指向函数的词法作用域,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过JS代码访问,它存在于JS引擎内部。下面是个经典的错误例子:

function foo(){
    var a = 2;
    this.bar();
}
function bar(){
    console.log(this.a);
}
foo();  //ReferenceError: a is not defined

这段代码首先通过this.bar()来引用函数bar(意外的成功了),通常省略前面的this,此外,开发者还试图用this联通foo()和bar()的词法作用域,使得bar()可以访问foo()作用域中的变量a,当然这是不可能的。至于为什么抛出这样一个异常,这里就不再赘述了。
this到底是一样什么样的机制???
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。(动态作用域的定义瞬间浮现在脑海里)。当一个函数被调用时,会创建一个活动记录(有时也叫执行上下文)。这个纪录会包含函数在哪里被调用(调用栈)、函数调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

调用位置

要理解this的绑定过程,首先要理解调用位置。


D86C4F31-669D-4A41-BFFF-8A7A4647B25D.png

下面的例子中的注释就分析了调用栈,显然易懂,不再赘述。

function baz(){
    //当前调用栈:baz
   //当前调用位置:全局作用域
   console.log("baz");
   bar();// <-- bar的调用位置
}
function bar(){
    //当前调用栈:baz->bar
    //当前调用位置:baz
    console.log("bar");
    foo(); <--foo的调用位置
}
function foo(){
    //当前调用栈:baz->bar->foo
    //当前调用位置:bar
    console.log("foo");
}
baz(); <-- baz的调用位置

绑定规则

首先找到调用位置,然后判断需要按照哪条绑定规则进行应用,如果多条规则都适用,则按优先级。

默认绑定

最常见的函数调用类型:独立函数调用

function foo(){
    console.log(this.a);
}
var a = 0;
foo();      //0

函数foo调用时应用了默认绑定,this指向了全局对象window。foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能应用默认绑定严格模式下,则不能将全局对象用于默认绑定,因此this会被绑定到undefined

隐式绑定

调用位置是否有上下文对象?是否被某个对象拥有或者包含? function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); //2 无论直接在obj中定义还是先定义在添加为引用属性,函数foo严格上来说都不属于这个对象,但是在函数被调用的时候,可以认为obj拥有或者包含这个函数,调用位置会使用obj上下文来引用函数。当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象。所以调用foo()时,this被绑定到obj,因而这里的this.a和obj.a就是一样的。对象属性引用链只有上一层在调用位置起作用
被隐式绑定的函数会丢失绑定对象,然后它应用默认绑定(非严格模式下)。

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    //fn其实引用的是foo
    fn();
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops,global";
doFoo(obj.foo); //“oops,global"

参数传递是一种隐式赋值,所以这种会造成隐式丢失(显式赋值也会造成隐式丢失)。

显式绑定

使用函数的call(...)和apply(...)方法。它们的第一个参数是对象,显然是为this准备的,在调用函数时,将它绑定到this。

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2
};
foo.call(obj);  //2

显式绑定一样无法解决之前的丢失绑定问题,但显式绑定的变种可以解决这个问题。

硬绑定
function foo(){
    console.log(this.a);
}
var obj = {
    a;2
};
var bar = function(){
    foo.call(obj);
};
bar();  //2
setTimeout(bar,100);    //2
//硬绑定的bar是无法再修改它的this
bar.call(window);   //2

首先我们创建了一个函数bar(),并在它的内部调用了foo.call(obj),所以我们强制把foo的this绑定到了obj。之后无论怎么调用函数bar,它都会再一次手动地在obj上调用foo。硬绑定是一种非常常用的内置方法。ES5提供了内置的方法Function.prototype.bind:

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind(obj);
var b =  bar(3);    //2 3;
console.(b);    //5
new绑定

使用new来调用函数,会自动执行如下操作:
1、构造一个新对象
2、这个新对象会被执行[[Protorype]]连接
3、这个新对象会被绑定到函数调用的this
4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

function foo(a){
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2

使用new来调用函数foo,我们会构造一个新对象并把它绑定到foo调用中的this上。

优先级

正常情况:new绑定>显式绑定>隐式绑定>默认绑定
特殊情况:当把null或者undefined作为绑定对象传入call、apply、bind,这些操作会被忽略,最后应用默认绑定。创建一个函数的“间接引用”,即前面提的绑定丢失,最终也是应用默认绑定。

软绑定

给默认绑定指定一个全局对象和undefined以外的值,那么就可以实现和硬绑定一样的效果,同时保留隐式绑定或显示绑定修改this的能力。

if(!Function.prototype.softBind){
    Function.prototype.softBind = function(obj){
        var fn = this;
        //捕获所以curried参数
        var curried = [].slice.call(arguments,1);
        var bound = function(){
            return fn.apply(
                (!this || this === (window || global))?
                    obj:this,
                curried.concat.apply(curried,arguments)
            );
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    };
}

首先检查调用时的this,如果this绑定到全局对象或undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。

ES6中的新玩法

箭头函数不是用function关键字定义的,而是用操作赋 => 定义的,这是ES6中新增的语法糖之一。箭头函数不适用于this的绑定规则,而是根据外层作用域来决定this,无论最外层绑定到了什么,它都会继承下来。
首先箭头函数的词法作用域:

function foo(){
    return (a) => {
        console.log(this.a);
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};
var bar = foo.call(obj1);
bar.call(obj2); //2

foo()内部的箭头函数会捕获调用foo()时的this,这里是obj1,所以bar的this绑定到了obj1,箭头函数的绑定无法修改。
箭头函数常用于回调函数中,例如事件处理器或定时器:

function foo(){
    setTimeout( () => { //这里的this在此法上继承自foo()
        console.log(this.a);
    },100);
}
var obj = {
    a:2
};
foo.call(obj);  //2

推荐阅读更多精彩内容