分析this绑定规则及优先级

476 阅读8分钟

1.为什么要用this

this提供了一种更优雅的方式来隐式“传递”一个对象引用,更加简洁并且易于复用。

var me = {
    name:'Kyle'
}
function identtify(){
    return this.name;
}
identtify.call(me);// Kyle

// 如果不使用this,需要给identtify()显示传入一个上下文对象
var me = {
    name:'Kyle'
}
function identtify(context){
    return context.name;
}
identtify(me);// Kyle

2.this两种误解

2.1 指向自身

console.log()打印了4次,说明foo()被调用了4次,但是为啥foo.count === 0? 执行foo.count = 0时候,向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向函数对象,所以虽然属性名相同,根对象并不相同。其实this.count无意中创建了一个全局变量count,它的值为NaN。

function foo(num) {
    console.log('foo:',num);
    // 记录foo被调用的次数
    this.count++;
}
foo.count = 0;
for (var index = 0; index < 10; index++) {
    if (index > 5) {
        foo(index)
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用多少次
console.log(foo.count);// 0

如果要从函数对象内部引用它自身,有下面两种方法:

// 第一种,通过一个指向函数对象的词法标识符(变量)来引用它
function foo(num) {
    console.log('foo:',num);
    // 记录foo被调用的次数
    foo.count++;
}
foo.count = 0;
for (var index = 0; index < 10; index++) {
    if (index > 5) {
        foo(index)
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用多少次
console.log(foo.count);// 4
 
// 第二种,强制this指向foo函数对象
function foo(num) {
    console.log('foo:',num);
    // 记录foo被调用的次数
    this.count++;
}
foo.count = 0;
for (var index = 0; index < 10; index++) {
    if (index > 5) {
        foo.call(foo,index)
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用多少次
console.log(foo.count);// 4

2.2 它的作用域

在某种情况下,它是正确的,但是在其他情况下他却是错误的。但是需要明确的是,this在任何情况下都不指向函数的词法作用域。 这段代码试图通过this.bar()来引用bar函数。这是不可能成功;此外还试图使用this联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里的变量a,这也是不可能实现的,不能使用this来引用一个词法作用域内部的东西。

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

3.this到底是什么?到底是一种什么样的机制?

this是在运行的时候进行绑定的,并不是编写时候绑定的。this的绑定和函数声明的位置没有任何关系,this的指向完全取决于函数的“调用位置”。

4.调用位置

“调用位置”就是函数在代码中被调用的位置,而不是声明位置。如何找到“函数被调用的位置”?需要分析“调用栈”(就是为了到达当前执行位置所调用的所有函数),“调用位置”就是在当前正在执行的函数的前一个调用中。

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的调用位置

5.绑定规则

函数在执行过程中调用位置如何决定this的绑定对象?首先需要找到调用位置,然后判断符合下面四条规则中的那一条(如果符合多条规则,那么是有优先级的,后面会谈到这点)。

5.1 默认绑定

在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。通过分析调用位置来看,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。

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


//  注意:如果使用严格模式,那么全局对象无法使用默认绑定,因为this会绑定到undefined
function foo() {
	// "use strict"
   console.log(this.a);
}
var a = 2;
foo();// this is undefined

5.2 隐式绑定

隐式绑定需要考虑调用位置是否有上下文对象。

当foo()被调用时,函数引用有上下文对象obj,根据隐式绑定规则,会把函数调用中的this绑定到这个上下文对象,因此this.a 等价于 obj.a。

function foo() {
   console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
obj.foo();// 2
 
// 注意:对象属性引用链中只有最后一层会影响调用位置
function foo() {
   console.log(this.a);
}

var obj2 = {
    a:42,
    foo:foo
}
var obj1 = {
    a:2,
    obj2:obj2
}
obj1.obj2.foo();// 42

隐式丢失:被隐式绑定的函数会丢失绑定对象,从而应用默认绑定,因此this绑定到了全局对象或undefined(严格模式)

在本例中,虽然bar()是obj.foo的一个引用,但是它引用的是foo函数本身,因此此时bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

function foo() {
   console.log(this.a);
}

var obj = {
    a:2,
    foo:foo
}
var bar = obj.foo;// 函数别名
var a = 3;// a是全局对象的属性
bar()// 3


// 发生在回调函数时的隐式丢失(参数传递其实就是一种隐式赋值,故结果和上面一样)
function foo() {
    console.log(this.a);
}
function doFoo(fun) {
    // fun其实引用的是foo
    fun();
}
var obj = {
    a:2,
    foo:foo
}
var a = 3;
doFoo(obj.foo); // 3

5.3显示绑定

call(...)和apply(...)方法,它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this。(它们的区别自行百度)

// 通过foo.call(...),我们可以在调用foo时强制把它的this绑定到obj
function foo() {
    console.log(this.a);
}
var obj = {
    a:2
}
foo.call(obj);// 2

但是,显示绑定仍然无法解决前面说的丢失绑定问题,因此有下面这种方式:

5.3.1硬绑定

应用场景:创建一个包裹函数,传入所有参数并返回接收到的所有值

// 通过foo.apply(obj,arguments);强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。
function foo(b) {
    console.log(this.a,b);
}
var obj = {
    a:2
}
var bar = function () {
    return foo.apply(obj,arguments);
}
bar(3); // 2 3
 
// 因为硬绑定是一种常用模式,在ES5中提供内置方法 Function.prototype.bind,bind(...)会返回一个硬绑定的新函数,用法如下:
function foo(b) {
    console.log(this.a,b);
}
var obj = {
    a:2
}
var bar = foo.bind(obj);
bar(3); // 2 3

5.4new 绑定

前言:在Js中,构造函数只是一些使用new操作符时被调用的函数。他们并不属于某个类,也不会实例化一个类,它们只是被new操作符调用的普通函数而已。(实际上并不存在所谓"构造函数",只有对于函数的"构造调用")。

使用new来调用函数,或说发生构造函数调用时候,会执行下面操作:

. 创建一个全新的对象

. 新对象会被执行[[原型]]连接

. 这个新对象会被绑定到函数调用的this

. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

// 使用new 来调用foo(...)时,生成一个新对象,并把新对象绑定到foo(...)中调用的this,这种绑定称之为new绑定
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2

6 优先级

6.1 默认绑定的优先级最低

6.2 显示绑定 > 隐式绑定

function foo(a) {
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    foo:foo
}
// 显而易见,显示绑定 > 隐式绑定
obj1.foo.call(obj2);// 3
obj2.foo.call(obj1);// 2

6.3 new绑定 > 隐式绑定

function foo(a) {
    this.a = a;
}
var obj = {
    foo:foo
}

obj.foo(2);
console.log(obj.a);// 2

var bar = new obj.foo(4);
console.log(obj.a);//2
// 显而易见,new绑定 > 隐式绑定
console.log(bar.a);//4

6.4 new绑定 > 显示绑定

new 和 call/apply无法一起使用,不能通过new foo.call(obj)来直接测试,这里使用硬绑定来测试他们的优先级。

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

var baz = new bar(3);
console.log(obj.a);//2
// 出乎意料,前面讲过foo(...)通过bind硬绑定后,foo(...)中this会被绑定到指定的对象,并返回一个新函数bar(...)。无论之后如何调用函数bar,它总会手动在指定的对象上调用foo。
// 但是,为何这里通过new 来调用bar的时候,this被重新绑定了呢?
// 原因:在ES5中内置的Function.prototype.bind(...)中是会判断硬绑定函数是否被new调用,如果是的话就是使用新创建的this替换硬绑定的this
console.log(baz.a);//3

总结:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

7.规则总有例外

7.1被忽略的this

如果把null或undefined作为this的绑定对象传入call,apply,bind,这些值在调用时会被忽略,实际应用默认绑定规则:

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

7.2间接引用

创建一个函数的“间接引用”,在这种情况下会应用默认绑定规则

function foo(a) {
    console.log(this.a);
}
var a = 2;
var obj1 = {
    a:3,
    foo:foo
}
var obj2 ={
    a:4
}
obj1.foo();// 3
(obj2.foo = obj1.foo)();//2

8.ES6中的箭头函数

箭头函数不使用this的四种规则标准,而是根据外层(函数或全局)作用域来决定this

function foo() {
    return (a)=>{
        //this继承foo()
        console.log(this.a);
    }
}
var obj1 = {
    a:2
}
var obj2 ={
    a:3
}
var bar = foo.call(obj1);
// foo()内部创建的箭头函数会捕获调用foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,并且箭头函数的绑定无法被修改。(new 也不行!)
bar.call(obj2);// 2 ,不是 3