this 五种绑定机制详解

384 阅读6分钟

前言

this绑定是Javascript开发中老生常谈的问题了,但要彻彻底底弄的很明白的人应该不多,有时候我也会被this绑定题弄的晕头转向的,本人今天就从this绑定的五种场景 默认绑定隐式绑定显式绑定new绑定箭头函数绑定 详解

一) this默认绑定

定义: 在没有应用其他规则时, this绑定遵循默认绑定, 但严格模式下与非严格模式下完全不同

看下面这个例子:

//非严格模式
var a = 2;
function fn(){
    console.log(this.a);    // 2
}
fn();

//严格模式
function foo(){
    "use strict";
    console.log(this.a);    // TypeError: Cannot read property 'a' of undefined
}
foo(); 

得出结论:

  • 非严格模式下,全局作用域中的函数调用时,函数词法作用域内的 this 指向全局对象 window
  • 严格模式下,函数调用时词法作用域内的 this 指向 undefined , 报 TypeError 错误

以上仅仅是举例,在实际开发中,不应混用严格与非严格模式

二) 隐式绑定

定义: 如果函数调用时,前面存在调用它的对象,那么函数作用域的this就用通过隐式绑定的方式获取对象this

看下面这个例子:

function foo() {
    console.log(this.name);
};
let obj = {
    name: '牧游',
    func: foo
};
obj.func() //牧游

提出疑问: 如果函数前面存在多个调用它的对象,那么this会隐式绑定到哪个对象上呢?

function foo() {
    console.log(this.name);
};
let obj = {
    name: '牧游',
    func: foo
};

let obj1 = {
    name: '张三',
    attr: obj
}
obj1.attr.func() //牧游

得出结论: 从上面例子的结果,可以发现,如果函数调用前存在多个对象, this指向距离调用自己最近的对象(也就是直接上级)

隐式丢失

在特定的场景下,隐式绑定也会存在丢失的问题,最常见的就是作为 参数传递 以及 变量赋值 ,还有就是 匿名自执行函数

我们先看下参数传递这个例子:

var name = '张三';
let obj = {
    name: '牧游',
    fn: function () {
        console.log(this.name);
    }
};

function fn1(param) {
    param();
};
fn1(obj.fn); // 张三

导致以上问题原因: obj.fn 被当做函数入参传递到fn1函数里去执行,obj.fn函数内部this并没有跟其所在对象绑定,所以this找不到就会指向window

第二个丢失问题:变量赋值

var name = '张三';
let obj = {
    name: '牧游',
    fn: function () {
        console.log(this.name);
    }
};
let fn1 = obj.fn;
fn1(); //张三

原因: 本质上与传参相同

第三个丢失问题:匿名自执行函数

    var a = 10;
    var foo = {
        a: 20,
        bar: {
            a: 30,
            fn: (function () {
                console.log(this.a); // 10
            })()
        }
    }

原因: 匿名函数的执行环境是全局性的,所以匿名函数的this指向是window

三) 显示绑定

定义: 通过call、apply以及bind方法改变this的行为

相比隐式绑定,能清楚的感知this指向变化过程

我们来看一下具体的例子:

let obj1 = {
    name: '张三'
};
let obj2 = {
    name: '李四'
};
let obj3 = {
    name: '王五'
}

let name = '牧游';

function fn() {
    console.log(this.name);
};
fn(); // 牧游
fn.call(obj1); // 张三
fn.apply(obj2); // 李四
fn.bind(obj3)(); // 王五

在上述代码中,分别通过call、apply、bind改变函数fn的this指向。其实这种做法,我们习惯称之为 函数调用

注意事项: 如果在使用call之类的方法改变this指向时,指向参数提供的是null 或者 undefined, 那么this将指向全局对象。比如下面这个例子:

let obj1 = {
    name: '张三'
};
let obj2 = {
    name: '李四'
};
let obj3 = {
    name: '王五'
}

let name = '牧游';

function fn() {
    console.log(this.name);
};
fn(); // 牧游
fn.call(undefined); // 牧游
fn.apply(null); // 牧游
fn.bind(undefined)(); // 牧游

另外, JS中部分API方法也内置了显示绑定,以forEach为例:

let obj = {
    name: '牧游'
};

[1, 2, 3].forEach(function () {
    console.log(this.name); //牧游 [3次]
}, obj);

四) new绑定

定义: 对类或者构造函数实例化的调用方式

function Fn() {
    this.name = '牧游';
}

let obj = new Fn();
console.log(obj.name);

五) this的绑定优先级

我们前面介绍this的四种绑定规则,这边提出一个问题,如果一个函数调用存在多种绑定方法,this最终指向谁呢?

这里我们直接先上答案,this绑定的优先级关系为:

显式绑定 > 隐式绑定 > 默认绑定

new绑定 > 隐式绑定 > 默认绑定

那么我们结合几个例子来验证下上面的规律,首先是 显示绑定 > 隐式绑定 :

//显式 > 隐式
let obj = {
    name: '张三',
    fn: function () {
        console.log(this.name);
    }
};
let obj1 = {
    name: '牧游'
};
obj.fn.call(obj1);// 牧游

其次是 new绑定 > 隐式绑定 :

//new > 隐式
let obj = {
    name: '张三',
    fn: function() {
        this.name = '牧游';
    }
}

let echo = new obj.fn();

console.log(echo.name); // 牧游

提出问题: 为什么显示绑定不和new绑定比较呢?

答: 因为不存在这种绑定同时生效的情景,如果同时写这两种代码会直接抛错,比如:

// error
let obj = {
    name: '张三',
    fn: function() {
    }
}

let obj1 = {
    name: '牧游'
}
let echo = new obj.fn.call(obj1);  // Uncaught TypeError: obj.fn.call is not a constructor

六) 箭头函数this

ES6的箭头函数是另类的存在,为什么要单独说呢,这是因为箭头函数中的this不适用上面介绍的四种绑定规则。

准备来说, 箭头函数中没有this, 箭头函数的this指向取决于外层作用域中的this, 外层作用域或函数的this指向谁, 箭头函数中的this便指向谁。比如下面这个例子:

function fn() {
    return () => {
        console.log(this.name);
    };
}
let obj1 = {
    name: '牧游'
};
let obj2 = {
    name: '张三'
};
let bar = fn.call(obj1); // fn this指向obj1
bar.call(obj2); // 牧游

提出问题: 为什么我们第一次绑定this并返回箭头函数后,再次改变this指向没生效呢?

答: 1) 箭头函数的this取决于外层作用域的this, fn函数执行是this指向了obj1, 所以函数的this也指向obj1。 2) 箭头函数this还有一个特性,就是 一旦箭头函数的this绑定成功,无法再次被修改

再次提出问题: 如果箭头函数有多个外层作用域,那最终该指向谁呢?

答: 箭头函数的this仅取决其直接外层作用域的this, 通俗一点说就是直接上级

function fn() {
    return function () {
        return () => {
            console.log(this.name);
        };
    }
}
let obj1 = {
    name: '牧游'
};
let obj2 = {
    name: '张三'
};
let obj3 = {
    name: '李四'
};
let bar = fn.call(obj1);
let foo = bar.call(obj2);
let echo = foo.call(obj3); // 张三

我们上面说了, "箭头函数的this一旦绑定成功,无法再次被修改", 其实我们通过可以修改外层函数this 指向达到间接修改箭头函数this的目的。比如下面这个例子:

function fn() {
    return () => {
        console.log(this.name);
    };
};

let obj1 = {
    name: '牧游'
};
let obj2 = {
    name: '张三'
};

fn.call(obj1)(); // fn this指向obj1,箭头函数this也指向obj1
fn.call(obj2)(); //fn this 指向obj2,箭头函数this也指向obj2

总结

通过上面的实例以及结合知识点讲解,总结了以下知识点:

  • 【默认绑定】严格模式与非严格模式this指向有所不同
  • 【隐式绑定】介绍了隐式丢失几种情况
  • 详细说明了this几种绑定优先级关系

本文在介绍过程中,如有不足之处,还请多多指出。

参考

掘金

木易杨前端进阶