关于 this 的一篇总结

198 阅读7分钟

前言

前排声明,这真的是写 this,没有什么太多新的东西,就是一个自己对 this 绑定规则的总结,也许后期水平提高会从更深的角度去解释 JavaScript 中的 this 绑定规则。本文从分别从绑定全局对象和绑定具体对象的角度总结了一下 this 的绑定规则。

绑定全局对象

this 绑定全局对象,分为两种情况:

  • 直接在全局作用域下使用 this
  • 被调用的函数不满足绑定具体对象的绑定规则

直接在全局作用域下使用 this

这种没什么好说的,全局作用域下调用 this 绑定的是全局对象,例如浏览器中绑定的是 window 对象。

console.log(this)

直接拿上面这段代码在浏览器控制台运行下得到的就是全局对象。

被调用的函数不满足绑定具体对象的绑定规则

直接使用函数名调用函数

function test () {
    console.log(this);  // window
}

test();

var test1 = function() {
    console.log(this);  // window
}

test1();

代码传送门

定义某对象的属性为函数,该属性值被赋值被另一变量时。

var aObj = {
    propertyFn: function() {
        console.log(this.testName);
    },
    testName: "aObj"
};

aObj.propertyFn();  //  "aObj"

var aFn = aObj.propertyFn;
aFn();  //  undefined

代码传送门

绑定具体对象

绑定具体对象的情况较多,可总结为以下几种

  • 函数作为对象属性被获取并调用
  • 使用 new 关键字调用函数
  • 使用 call,apply,bind 调用函数
  • 函数被注册为事件监听器和事件处理器
  • 箭头函数中的 this

函数作为对象属性被获取并调用

函数作为对象属性被获取并调用时,函数中的 this 绑定的是获取该属性的对象。考虑如下代码

function test() {
    return this.testName;
}

var testObj = {
    testName: "testObj",
    getTestName: test,
    getTestNameFn: function() {
        return this.testName;
    }
}

console.log(testObj.getTestName()); // "testObj" 函数虽然是在全局作用域下定义的,
                                    // 但是被赋值给了testObj的getTestName属性,且是被作为对象的属性调用的
console.log(testObj.getTestNameFn());   // "testObj"

代码传送门

使用 new 关键字调用函数

使用 new 关键字调用函数时,该函数会生成并返回以个新的对象,函数中的 this 绑定的生成的新对象。

function Test(name) {
    this.name = name;
    console.log(this);
}

var s = new Test("s");  //  {name: "s"}
function Test1(name) {
    this.name = name;
    console.log(this);
    return true;
}

var s1 = new Test1("s1");   //  {name: "s1"}

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

var s2 = new Test2("s2");   //  {name: "s2"}

代码传送门

上述结果表明,使用 new 调用函数的时候一定会生成一个新对象,且 this 绑定的就是这个新对象,只不过当你在函数中 return 了非 Object 类型的值时,这个对象不会被赋值给你定义的接收变量,这时接收的变量被赋的是函数中使用 return 返回的值。

使用call ,apply,dind 调用函数

这里call和apply的作用是类似的,都是函数的实例方法,可为函数指定 this 绑定的对象,两者区别在于 apply 的第二个参数是数组,该数组中的值会以实参形式被传递给调用 apply 的函数,而 call 函数除了第一个参数外的参数均被传递给调用 call 的函数。

function test(param1, param2) {
  console.log(this.name, param1 + ", " + param2);
}

var a = {
  name: "a"
};

var b = {
  name: "b"
}

test.call(a, "aParam1", "bParam2");
test.apply(b, ["bParam1", "bParam2"]);
test();

代码传送门

bind函数的作用和以上两者与别很大,其作用是将函数中的 this 绑定对象与指定的对象绑定起来,返回一个函数,每次调用返回的函数时,其 this 都是绑定的指定对象。

function test() {
    console.log(this.name);
}

var a = {
    name: "a"
}

var bindTest = test.bind(a);

bindTest(); // "a"

var b = {
    name: "b",
    getName: bindTest
}

b.getName();    // "a"

var c = new bindTest(); // undefined

bindTest.call(b);   // "a"

代码传送门

从上面的示例代码可以看出,函数和指定对象被绑定后使用 new 关键字是绑定失效,在以上示例中绑定函数中的 this 绑定的是一个新创建的对象实例,且该对想的构造函数时test函数。由此也可得出,this 绑定场景同时出现的情况下 new 的优先级是高于调用 bind 函数的优先级的。

箭头函数

关于箭头函数中 this 绑定,MDN 中的说法是箭头函数是没有自己的 this 的,其 this 是从其作用域链上层作用域继承而来的。那么怎么理解呢?下面上代码:

let arrowFn = () => {console.log(this === window)};
arrowFn();  // true

let a = {
    name: "a",
    getSelf: arrowFn
};
a.getSelf();    // true

let b = {
    name: "b"
}
arrowFn.call(b);    // true

代码传送门

以上代码是箭头函数直接在全局作用域下定义的情况,那么其作用域链上层就是全局作用域,而在浏览器中全局作用域 this 绑定的值是 window 。由于 JavaScript 中的作用域是静态作用域,那么箭头函数在全局作用域中定义时便已经可以确定其 this 就是 window 了,而且后面的该箭头函数作为对象属性值被调用,还是使用 call 显示指定 this 其 this 均为改变。而非箭头函数的画风是这样的:

let fn = function() {
    console.log(this === window);
}
fn();   // true
let a = {
    name: "a",
    getSelf: fn
};
a.getSelf();    // false

let b = {
    name: "b"
}
fn.call(b);    // false

代码传送门

那么是不是一旦箭头函数被定义了,其 this 的绑定就已经被确定了呢?

let createArrowFn = function() {
    return () => {console.log(this)};
}

let a = {
    name: "a",
    getSelf: createArrowFn
};
let aArrow = a.getSelf();   
aArrow();    // 对象a

let b = {
    name: "b"
}
var bArrow = createArrowFn.call(b);  
bArrow();   // {name: "b"}

代码传送门

上面代码两次 this 打印的结果是不一样的,那么是不是就推翻了箭头函数一旦被定义,其 this 就已经确定了的结论。其实不然,这里箭头函数的是上层作用域是createArrowFn这个函数的作用域,这个函数作用域中的 this 会随着调用场景的不同发生发生变化,所以继承其作用域绑定 this 的箭头函数中的 this 自然也会发生改变了。其实箭头函数中的 this 可以这么理解,相当于将函数的上层作用域的 this 用一个变量保存下来,然后在其子函数中使用它。

let fn = function() {
    var _this = this;
    return function() {
        console.log(_this);
    }
}

let a = {
    name: "a",
    getSelf: createArrowFn
};
let aArrow = a.getSelf();   
aArrow();    // 对象a

let b = {
    name: "b"
}
var bArrow = createArrowFn.call(b);  
bArrow();   // {name: "b"}

代码传送门

在理解箭头函数中的 this 时只需理解其被定义时所在作用域的 this 绑定的是什么就可以了。

事件监听器和事件处理器中的 this

先说明不管是事件监听器还是事件处理器中 this 绑定的都是当前触发该事件的节点,即绑定了该事件的元素节点。

这里主要是区分下事件监听器和事件处理器,事件处理器其实是指 html 标签的 on... 属性定义的函数,比如 onclick="function() {}",当然也可以在 JavaScript 中去设置该属性,事件处理器的特点是其只能有一个,因为是 html 标签属性所以可以覆盖。

事件监听器是指使用addEventListener函数注册的事件回调函数,可同时注册多个。

结论

在讨论 JavaScript 中 this 的绑定值时,其实就是几种情况:

  1. new 函数时this是新创建的对象
  2. call, apply, bind 指定的对象
  3. 调用该函数的对象
  4. 箭头函数的 this 取决于其定义时所在作用域的 this 绑定值
  5. 事件监听器和事件处理器的中的 this 绑定的是绑定函数节点
  6. 上面的情况都不是时,严格模式下的 undefined ,非严格模式下的全局变量

以上总结仅仅为粗浅的不同场景下 this 的绑定值的总结,没有从更深的层次(比如ES标准中的定义)去讨论,主要个人时间水平有限,所以大佬请忽略。如有错误欢迎各位指正,不胜感激。