JS中的"this"到底指向哪里?

115 阅读5分钟

为什么要用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(me); //KYLE
identify(you); //READER
speak(me); //Hello, I'm KYLE
speak(you); //Hello, I'm READER

使用this代替上下文

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

this到底指向哪里?

通常有个误区,都认为this指向函数所在的作用域。其实不是,this在任何情况下都既不指向函数本身,也不指向函数的词法作用域(词法作用域:在哪定义,作用域就在哪)。

!!this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

判定this指向只需要2步

找到函数调用位置 + 判断绑定规则

规则一、默认绑定

function foo() {
    console.log(this.a);
}
var a = 2;
foo(); // 2;全局作用域中调用,this指向全局对象

⚠️ 在严格模式下,函数中的this值为undefined,而不是全局对象。let默认在严格模式下工作。

规则二、隐式绑定(调用上下文:就近原则)

比如对象属性引用链

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2;就近原则,调用上下文是obj
function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42;就近原则,最后一层调用位置是obj2
隐式绑定很不稳定,有时候它会丢失

比如引用赋值:

我们把上面第一个例子的代码改一改,把foo()的引用赋值给bar,再调用。

var a = "oops, global!!"
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo;
bar(); //oops, global!!

为什么会出现这种情况呢?这是因为bar是对foo的函数引用,不带任何修饰,等同于在全局环境中直接调用foo.

比如回调函数

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    // fn其实引用的是foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo(obj.foo); // "oops, global"

其实还是同一个原理函数引用,fn是对foo的引用,仍然等同于在全局环境中直接调用foo.

再比如内置函数

也是同样的原理函数引用,也会造成隐式绑定丢失。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout(obj.foo, 100); // "oops, global"

规则三、显式绑定

call, apply, bind

apply与 call()几乎完全相同,只是函数参数在 call() 中逐个作为列表传递,而在 apply() 中它们会组合在一个对象中,通常是一个数组——例如,func.call(this, "eat", "bananas") 与 func.apply(this, ["eat", "bananas"])。一旦绑定后不能再修改它的this。

三者的区别在于: call,apply会马上执行方法,而bind不会马上执行,而是先返回一个函数,这个函数绑定了this和准备好了对应的参数值。以后要用时直接拿来用。通常情况下,你可以将 const boundFn = fn.bind(thisArg, arg1, arg2) 和 const boundFn = (...restArgs) => fn.call(thisArg, arg1, arg2, ...restArgs) 构建的绑定函数的调用效果视为等效(但就构建 boundFn 的过程而言,不是二者等效的)。总之一句话,bind会创建新的函数,并且可以在绑定函数上创建另一个绑定函数。

call示例

var a = "oops, global!!"
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function() {
foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2 无效!!硬绑定后无法再修改

apply示例

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

bind示例 1

"use strict"; // 防止 `this` 被封装到到包装对象中
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.log(b); // 5

bind示例 2 - 绑定函数基础上创建另一个绑定函数

"use strict"; // 防止 `this` 被封装到到包装对象中

function log(...args) {
  console.log(this, ...args);
}
const boundLog = log.bind("this value", 1, 2);
const boundLog2 = boundLog.bind("new this value", 3, 4);
boundLog2(5, 6); // "this value", 1, 2, 3, 4, 5, 6

调用boundLog2时,是先调用boundLog,再调用log

log最后接收参数的顺序是boundLog绑定的参数,boundLog2绑定的参数,boundLog2接收的参数。

规则四、new绑定

JS里面的new有别于其他语言,new并不是构造函数,而是对于函数的“构造调用”

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2); //构造一个新对象bar,并把它绑定到foo()调用中的this上
console.log(bar.a); // 2

箭头函数里的this?

日常工作中,我们常常用var sel = this来解决this绑定问题。原理是我们所熟知的词法作用域

var obj = {
    count: 0,
    cool: function coolFn() {
        var self = this;
        if (self.count < 1) {
            setTimeout(function timer(){
            self.count++;
                console.log("awesome? ");
            }, 100 );
        }
    }
};
obj.cool(); // awesome?

其实箭头函数同理, 它放弃了所有普通this绑定的规则(上面4个规则),取而代之的是用当前的词法作用域覆盖了this本来的值

var obj = {
    count: 0,
    cool: function coolFn() {
        if (this.count < 1) {
        setTimeout(() => {
            this.count++;
            console.log("awesome? ");
            }, 100 );
        }
    }
};
obj.cool(); // awesome?