JavaScript 的 this 指向题目深度剖析

JavaScript 中的 this 指向题目有许多博客在诠释,依然有许多人问。上周我们的开辟团队一连两个人碰到相干题目,所以我不得不将关于前端构建手艺的交流会延长了半个时刻议论 this 的题目。

与我们罕见的许多言语差别,JavaScript 函数中的 this 指向并不是在函数定义的时刻肯定的,而是在挪用的时刻肯定的。换句话说,函数的挪用体式格局决议了 this 指向

JavaScript 中,一般的函数挪用体式格局有三种:直接挪用、要领挪用和 new 挪用。除此之外,另有一些迥殊的挪用体式格局,比方经由过程 bind() 将函数绑定到对象以后再举行挪用、经由过程 call()apply() 举行挪用等。而 es6 引入了箭头函数以后,箭头函数挪用时,其 this 指向又有所差别。下面就来剖析这些状况下的 this 指向。

直接挪用

直接挪用,就是经由过程 函数名(...) 这类体式格局挪用。这时刻,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在 NodeJs 中全局对象是 global

来看一个例子:

// 简朴兼容浏览器和 NodeJs 的全局对象
const _global = typeof window === "undefined" ? global : window;

function test() {
    console.log(this === _global);    // true
}

test();    // 直接挪用

这里须要注重的一点是,直接挪用并不是指在全局作用域下举行挪用,在任何作用域下,直接经由过程 函数名(...) 来对函数举行挪用的体式格局,都称为直接挪用。比方下面这个例子也是直接挪用

(function(_global) {
    // 经由过程 IIFE 限制作用域

    function test() {
        console.log(this === _global);  // true
    }

    test();     // 非全局作用域下的直接挪用
})(typeof window === "undefined" ? global : window);

bind() 对直接挪用的影响

另有一点须要注重的是 bind() 的影响。Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数不论以什么样的体式格局挪用,其 this 一直指向绑定的对象。照样来看例子:

const obj = {};

function test() {
    console.log(this === obj);
}

const testObj = test.bind(obj);
test();     // false
testObj();  // true

那末 bind() 干了啥?无妨模仿一个 bind() 来相识它是怎样做到对 this 发生影响的。

const obj = {};

function test() {
    console.log(this === obj);
}

// 自定义的函数,模仿 bind() 对 this 的影响
function myBind(func, target) {
    return function() {
        return func.apply(target, arguments);
    };
}

const testObj = myBind(test, obj);
test();     // false
testObj();  // true

从上面的示例能够看到,起首,经由过程闭包,坚持了 target,即绑定的对象;然后在挪用函数的时刻,对原函数运用了 apply 要领来指定函数的 this。固然原生的 bind() 完成能够会差别,而且更高效。但这个示例说清晰明了 bind() 的可行性。

call 和 apply 对 this 的影响

上面的示例顶用到了 Function.prototype.apply(),与之类似的另有 Function.prototype.call()。这两要领的用法请人人自身经由过程链接去看文档。不过,它们的第一个参数都是指定函数运转时个中的 this 指向。

不过运用 applycall 的时刻依然须要注重,假如目次函数自身是一个绑定了 this 对象的函数,那 applycall 不会像预期那样实行,比方

const obj = {};

function test() {
    console.log(this === obj);
}

// 绑定到一个新对象,而不是 obj
const testObj = test.bind({});
test.apply(obj);    // true

// 希冀 this 是 obj,即输出 true
// 然则因为 testObj 绑定了不是 obj 的对象,所以会输出 false
testObj.apply(obj); // false

因而可知,bind() 对函数的影响是深远的,慎用!

要领挪用

要领挪用是指经由过程对象来挪用其要领函数,它是 对象.要领函数(...) 如许的挪用情势。这类状况下,函数中的 this 指向挪用该要领的对象。然则,一样须要注重 bind() 的影响。

const obj = {
    // 第一种体式格局,定义对象的时刻定义其要领
    test() {
        console.log(this === obj);
    }
};

// 第二种体式格局,对象定义好以后为其附加一个要领(函数表达式)
obj.test2 = function() {
    console.log(this === obj);
};

// 第三种体式格局和第二种体式格局道理雷同
// 是对象定义好以后为其附加一个要领(函数定义)
function t() {
    console.log(this === obj);
}
obj.test3 = t;

// 这也是为对象附加一个要领函数
// 然则这个函数绑定了一个不是 obj 的别的对象
obj.test4 = (function() {
    console.log(this === obj);
}).bind({});

obj.test();     // true
obj.test2();    // true
obj.test3();    // true

// 受 bind() 影响,test4 中的 this 指向不是 obj
obj.test4();    // false

这里须要注重的是,后三种体式格局都是预定定义函数,再将其附加给 obj 对象作为其要领。再次强调,函数内部的 this 指向与定义无关,受挪用体式格局的影响。

要领中 this 指向全局对象的状况

注重这里说的是要领中而不是要领挪用中。要领中的 this 指向全局对象,假如不是因为 bind(),那就肯定是因为不是用的要领挪用体式格局,比方

const obj = {
    test() {
        console.log(this === obj);
    }
};

const t = obj.test;
t();    // false

t 就是 objtest 要领,然则 t() 挪用时,个中的 this 指向了全局。

之所以要迥殊提出这类状况,重要是因为常常将一个对象要领作为回调传递给某个函数以后,却发明运转结果与预期不符——因为疏忽了挪用体式格局对 this 的影响。比方下面的例子是在页面中对某些事变举行封装以后迥殊轻易碰到的题目:

class Handlers {
    // 这里 $button 假定是一个指向某个按钮的 jQuery 对象
    constructor(data, $button) {
        this.data = data;
        $button.on("click", this.onButtonClick);
    }

    onButtonClick(e) {
        console.log(this.data);
    }
}

const handlers = new Handlers("string data", $("#someButton"));
// 对 #someButton 举行点击操纵以后
// 输出 undefined
// 但预期是输出 string data

this.onButtonClick 作为一个参数传入 on() 以后,事宜触发时,理论上是对这个函数举行的直接挪用,而不是要领挪用,所以个中的 this 会指向全局对象 —— 但实际上因为挪用事宜处置惩罚函数的时刻,this 指向会绑定到触发事宜的 DOM 元素上,所以这里的 this 是指向触发事宜的的 DOM 元素(注重:this 并不是 jQuery 对象),即 $button.get(0)(注重代码前解释中的假定)。

要处理这个题目有许多种要领:

// 这是在 es5 中的处理办法之一
var _this = this;
$button.on("click", function() {
    _this.onButtonClick();
});

// 也能够经由过程 bind() 来处理
$button.on("click", this.onButtonClick.bind(this));

// es6 中能够经由过程箭头函数来处置惩罚,在 jQuery 中慎用
$button.on("click", e => this.onButtonClick(e));

不过请注重,将箭头函数用作 jQuery 的回调时形成要警惕函数内对 this 的运用。jQuery 大多数回调函数(非箭头函数)中的 this 都是示意挪用目的,所以能够写 $(this).text() 如许的语句,但 jQuery 没法转变箭头函数的 this 指向,一样的语句语义完整差别。

new 挪用

在 es6 之前,每个函数都能够看成是组织函数,经由过程 new 挪用来发生新的对象(函数内无特定返回值的状况下)。而 es6 转变了这类状况,虽然 class 定义的类用 typeof 运算符获得的依然是 "function",但它不能像一般函数一样直接挪用;同时,class 中定义的要领函数,也不能看成组织函数用 new 来挪用。

而在 es5 中,用 new 挪用一个组织函数,会建立一个新对象,而个中的 this 就指向这个新对象。这没有什么牵挂,因为 new 自身就是设想来建立新对象的。

var data = "Hi";    // 全局变量

function AClass(data) {
    this.data = data;
}

var a = new AClass("Hello World");
console.log(a.data);    // Hello World
console.log(data);      // Hi

var b = new AClass("Hello World");
console.log(a === b);   // false

箭头函数中的 this

先来看看 MDN 上对箭头函数的申明

An arrow function expression has a shorter syntax than a function expression and does not bind its own this, arguments, super, or new.target. Arrow functions are always anonymous. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

这里已清晰了说清晰明了,箭头函数没有自身的 this 绑定。箭头函数中运用的 this,实际上是直接包括它的谁人函数或函数表达式中的 this。比方

const obj = {
    test() {
        const arrow = () => {
            // 这里的 this 是 test() 中的 this,
            // 由 test() 的挪用体式格局决议
            console.log(this === obj);
        };
        arrow();
    },

    getArrow() {
        return () => {
            // 这里的 this 是 getArrow() 中的 this,
            // 由 getArrow() 的挪用体式格局决议
            console.log(this === obj);
        };
    }
};

obj.test();     // true

const arrow = obj.getArrow();
arrow();        // true

示例中的两个 this 都是由箭头函数的直接外层函数(要领)决议的,而要领函数中的 this 是由其挪用体式格局决议的。上例的挪用体式格局都是要领挪用,所以 this 都指向要领挪用的对象,即 obj

箭头函数让人人在运用闭包的时刻不须要太纠结 this,不须要经由过程像 _this 如许的局部变量来暂时援用 this 给闭包函数运用。来看一段 Babel 对箭头函数的转译能够能加深明白:

// ES6
const obj = {
    getArrow() {
        return () => {
            console.log(this === obj);
        };
    }
}    
// ES5,由 Babel 转译
var obj = {
    getArrow: function getArrow() {
        var _this = this;
        return function () {
            console.log(_this === obj);
        };
    }
};

别的须要注重的是,箭头函数不能用 new 挪用,不能 bind() 到某个对象(虽然 bind() 要领挪用没题目,然则不会发生预期结果)。不论在什么状况下运用箭头函数,它自身是没有绑定 this 的,它用的是直接外层函数(即包括它的近来的一层函数或函数表达式)绑定的 this

订正

  • this.onButtonClick 用于 jQuery 事宜的时刻,this 已被 jQuery 改成指向触发事宜的元素,谢谢 @玉轮哥哥@QoVoQ 指出。此毛病已在文中修改了。
    原文作者:边城
    原文地址: https://segmentfault.com/a/1190000008400124
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞