7

一文梳理JavaScript中的this

 3 years ago
source link: http://www.cnblogs.com/rainbowly/p/13167655.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近零零碎碎看了许多关于this的文章,本着“好记性不如烂笔头”的思想,特在这里整理一下this有关的知识点。【长文警告!!!】

接下来,笔者将按照以下目录对 this 进行阐述:

  • this是什么?
  • this指向
    • this在全局范围内
    • this在对象的构造函数内
    • this在对象的方法内
    • this在简单函数内
    • this在箭头函数内
    • this在一个事件侦听器内
  • this绑定规则
    • 默认绑定
    • 隐式绑定
    • 显示绑定(this修改)
    • 优先级
  • 箭头函数

1. this是什么?

this是JavaScript的一个关键字,但它时常蒙着面纱让人无法捉摸,许多对this不明就里的同学,常常会有这样的错误认知:

  • this在函数内指向函数自身

    • function foo(num){
            console.log("foo: " + num);
        
            //记录foo被调用次数
            this.count++;
        }
        foo.count = 0;
        for(let i=0; i<10; i++){
            if(i > 5){
                foo(i);
            }
        }
        console.log(foo.count); // 0, this并没有指向foo函数,foo.count没有进行任何操作
  • this在函数内指向函数的作用域

    • function foo(){
            var a = 2;
            this.bar();
        }
        function bar(){
            console.log(this.a);
        }
        foo();// undefined, window对象没有bar这一属性

2. this指向

this的指向取决于他所处的环境. 大致上,可以分为下面的6种情况:

  • this在全局范围内
  • this在对象的构造函数内
  • this在对象的方法内
  • this在一个简单的函数内
  • this在箭头函数内
  • this在一个事件侦听器内

2.1 this在全局范围内

this在全局范围内绑定什么呢?这个相信只要学过JS,应该都知道答案。如果不知道,同学真的应该反思自己的学习态度和方法是否存在问题了。话不多说,直接上代码,一探究竟,揭开this在全局范围下的真面目:

console.log(this); // Window

不出意外, this在全局范围内指向window对象 ()。通常, 在全局环境中, 我们很少使用this关键字, 因此对它也没那么在意. 让我们继续看下一个环境.

2.2 this在对象的构造函数内

当我们使用new创建构造函数的实例时会发生什么呢?以这种方式调用构造函数会经历以下四个步骤:

  • 创建一个空对象;

  • 将构造函数的作用域赋给新对象(this指向了这个新对象),继承函数的原型;

  • 执行构造函数中的代码;

  • 返回新对象。

看完上面的内容,大家想必也知道this在对象的构造函数内的指向了吧! 当你使用new关键字创建一个对象的新的实例时, this关键字指向这个实例 .

举个栗子:

function Human (age) {
    this.age = age;
}
let greg = new Human(22);
let thomas = new Human(24);

console.log(greg); // this.age = 22
console.log(thomas); // this.age = 24

// answer
Person { age:22}
Person { age:24}

2.3 this在对象方法内

方法是与对象关联的函数的通俗叫法, 如下所示:

let o = {
    sayThis(){
        console.log(this);
    }
}

ii6NBjE.png!web

如上所示, 在对象的任何方法内的this都是指向对象本身 .

好了,继续下一个环境!

2.4 this在简单函数内

可能看到这里,许多同学心里会有疑问,什么是简单函数?

其实简单函数大家都很熟悉,就像下面一样,以相同形式编写的匿名函数也被认为是简单函数(非箭头函数)。

function hello(){
    console.log("hello"+this);
}

这里需要注意, 在浏览器中,不管函数声明在哪里,匿名或者不匿名,只要不是直接作为对象的方法,this指向始终是window对象 (除非使用call,apply,bind修改this指向)。

举个栗子说明一下:

// 显示函数,直接定义在sayThis方法内,this指向依旧不变
function simpleFunction() {
    console.log(this);
}

var o = {
    sayThis() {
        simpleFunction();
    }
}

simpleFunction(); // Window
o.sayThis(); // Window


// 匿名函数
var o = {
    sayThis(){
        (function(){consoloe.log(this);})();
    }
} 
o.sayThis();// Window

对于初学者来说,this在简单函数内的表现时常让他们懵逼不已, 难道this不应该指向对象本身? 这个问题曾经也出现在我的脑海里过,没错,在写代码时我也踩过这个坑。

通常的,当我们要在对象方法内调用函数,而这个函数需要用到this时,我们都会创建一个变量来保存对象中的this的引用. 通常, 这个变量名称叫做self或者that。具体说下所示:

const o = {
    doSomethingLater() {
        const self = this;
        setTimeout(function() {
            self.speakLeet();
        }, 1000);
    },
    speakLeet() {
        console.log(`1337 15 4W350M3`);
    }
}

o.doSomethingLater(); // `1337 15 4W350M3`

心细的同学可能已经发现,这里的简单函数没有将箭头函数包括在内,那么下一个环境是什么想必也能猜到啦,那么现在进入下一个环境,看看this指向什么。

2.5 this在箭头函数内

和简单函数表现不太一样, this在箭头函数中总是跟它在箭头函数所在作用域的this一样(在它直接作用域). 所以, 如果你在对象中使用箭头函数, 箭头函数中的this总是指向这个对象本身, 而不是指向Window.

下面我们使用箭头函数,重写一下上面的案例:

const o = {
    doSomethingLater() {
        setTimeout(() => this.speakLeet(), 1000);
    },
    speakLeet() {
        console.log(`1337 15 4W350M3`);
    }
}
o.doSomethingLater(); // `1337 15 4W350M3`

最后,让我们来看看最后一种环境 - 事件侦听器.

2.6 this在事件侦听器内

在事件侦听器内, this被绑定的是触发这个事件的元素:

let button = document.querySelector('button');

button.addEventListener('click', function() {
    console.log(this); // button
});

3. this绑定规则

事实上,只要记住上面this在不同环境的绑定值,足以应付大部分工作。然而,好学的同学总是会忍不住想说,为什么呢?对,为什么this在这些情况下绑定这些值呢? 学习,我们不能只知其然,而不知所以然 。所以,现在就让我们来探寻,this值获取的真相吧。

现在,让我们回忆一下,在讲什么是this的时候,我们说到“ this的绑定取决于他所处的环境 ”。这句话其实不是十分准确,准确的说, this不是编写时绑定,而是运行时绑定 。它 依赖于函数调用的上下文条件this绑定和函数声明的位置无关,反而和函数被调用的方式有关

当一个函数被调用时,会建立一个活动记录,也称为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。 this实际上是在函数被调用时建立的一个绑定,它指向什么是完全由函数被调用的调用点来决定的

仅仅是规则

现在我们将注意力转移到调用点 如何 决定在函数执行期间this指向哪里。

你必须考察call-site并判定4种规则中的哪一个适用。我们将首先独立的解释一下这4种规则中的每一种,之后我们来展示一下如果有多种规则可以适用调用点时,它们的优先级。

3.1 默认绑定规则

第一种规则来源于函数调用的最常见的情况:独立函数调用。可以认为这种this规则是在没有其他规则适用时的默认规则。我们给它一个称呼“默认绑定”.

现在来看这段代码:

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

当foo()被调用时,this.a解析为我们的全局变量a。为什么?因为在这种情况下,对此方法调用的this实施了 默认绑定,所以使this指向了全局对象。

在我们的代码段中,foo()是被一个直白的,毫无修饰的函数引用调用的。没有其他的我们将要展示的规则适用于这里,所以 默认绑定 在这里适用。

如果strict mode在这里生效,那么对于 默认绑定 来说全局对象是不合法的,所以this将被设置为undefined。

'use strict'
function foo(){
    console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a = 1;
foo();
function foo(){
	'use strict'
    console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a = 1;
foo();

微妙的是,即便所有的this绑定规则都是完全基于调用点,如果foo()的 内容 没有在strint mode下执行,对于 默认绑定 来说全局对象是 唯一 合法的;foo()的call-site的strict mode状态与此无关。

function foo(){
    console.log(this.a); 
}
var a = 1;
(function(){
	'use strict';
	foo(); // 1
})();

注意: 在代码中故意混用strict mode和非strict mode通常是让人皱眉头的。你的程序整体可能应当不是 Strict 就是非Strict。然而,有时你可能会引用与你的 Strict 模式不同的第三方包,所以对这些微妙的兼容性细节要多加小心。

3.2 隐式绑定

另一种要考虑的规则是:调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象。

让我们来看这段代码:

function foo() {
    console.log(this.a);
}
let o = {
    a: 2,
    foo,
}
o.foo(); // 2

这里,我们注意到foo函数被声明然后作为对象o的方法,无论foo()是否一开始就在obj上被声明,还是后来作为引用添加(如上面代码所示),都是这个 函数 被obj所“拥有”或“包含”。这里,调用点使用obj环境来引用函数,所以可以说 obj对象在函数被调用的时间点上“拥有”或“包含”这个 函数引用。

当一个方法引用存在一个环境对象时,隐式绑定 规则会说:是这个对象应当被用于这个函数调用的this绑定。

只有对象属性引用链的最后一层是影响调用点的。比如:

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

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

隐式绑定的隐患

当一个 隐含绑定丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据strict mode的状态,结果不是全局对象就是undefined。

下面来看这段代码:

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

var obj = {
    a:2,
    foo
};
var bar = obj.foo;
var a = "Global variable";
bar(); // "Global variable"

尽管bar似乎是obj.foo的引用,但实际上它只是另一个foo自己的引用而已。另外,起作用的调用点是bar(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。

这种情况发生的更加微妙,更常见,更意外的方式,是当我们考虑传递一个回调函数时:

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

function doFoo(fn){
	fn();
}

var obj = {
    a:2,
    foo,
};
var a = "Global variable";
dooFoo(obj.foo); // "Global variable"

参数传递仅仅是一种隐含的赋值,而且因为我们在传递一个函数,它是一个隐含的引用赋值,所以最终结果和我们前一个代码段一样。同样的,语言内建,如setTimeout也一样,如下所示

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

var obj = {
    a:2,
    foo,
};
var a = "Global variable";
setTimeout(obj.foo, 100); // "Global variable"

把这个粗糙的setTimeout()假想实现当做JavaScript环境内建的实现的话:

function setTimeout(fn, delay){
    // 等待delay毫秒
    fn();
}

正如我们看到的, 隐含绑定丢失了它的绑定是十分常见的,不管哪一种意外改变this的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。但是我们可以使用显示绑定强行固定this。

3.3 显示绑定

我们看到隐含绑定,需要我们不得不改变目标对象使它自身包含一个对函数的引用,而后使用这个函数引用属性来间接地(隐含地)将this绑定到这个对象上。

但是,如果你想强制一个函数调用使用某个特定对象作为this绑定,而不在这个对象上放置一个函数引用属性呢?

js有提供call()、apply()方法,ES5中也提供了内置的方法 Function.prototype.bind,可以引用一个对象时进行强制绑定调用。

考虑这段代码:

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

通过foo.call(..)使用 明确绑定 来调用foo,允许我们强制函数的this指向obj。

如果你传递一个简单原始类型值(string,boolean,或 number类型)作为this绑定,那么这个原始类型值会被包装在它的对象类型中(分别是new String(..),new Boolean(..),或new Number(..))。这通常称为“boxing(封箱)”。

注意: 就this绑定的角度讲,call(..)和apply(..)是完全一样的。它们确实在处理其他参数上的方式不同,但那不是我们当前关心的。

单独依靠call和apply,仍然可能出现函数“丢失”自己原本的this绑定,或者被第三方覆盖等问题。

但有一个技巧可以避免出现这些问题

考虑这段代码:

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

我们创建了一个函数bar(),在它的内部手动调用foo.call(obj),由此强制this绑定到obj并调用foo。无论你过后怎样调用函数bar,它总是手动使用obj调用foo。这种绑定即明确又坚定,该方法被开发者称为 硬绑定 (显示绑定的变种)(hard binding)

用硬绑定将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道:

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);
console.log(b); //  5

另一种表达这种模式的方法是创建一个可复用的帮助函数:

function foo(something){
    console.log(this.a, something);
    return this.a + something;
}

function bind(fn, obj){
    return function(){
        return fn.apply(obj, arguments);
    };
}

var obj = { a:2};
var bar = bind(foo, obj);
var b = bar(3);
console.log(b); // 5

由于 硬绑定 是一个如此常用的模式,它已作为ES5的内建工具提供,即前文提到的Function.prototype.bind:

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

bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用原本的函数。

注意: 在ES6中,bind(..)生成的硬绑定函数有一个名为.name的属性,它源自于原始的 目标函数(target function)。举例来说:bar = foo.bind(..)应该会有一个bar.name属性,它的值为"bound foo",这个值应当会显示在调用栈轨迹的函数调用名称中。

3.4new 绑定

第四种也是最后一种this绑定规则

当在函数前面被加入new调用时,也就是构造器调用时,下面这些事情会自动完成:

  • 一个全新的对象会凭空创建(就是被构建)
  • 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  • 这个新构建的对象被设置为函数调用的this绑定
  • 除非函数返回一个它自己的其他 对象,这个被new调用的函数将 自动 返回这个新构建的对象。

考虑这段代码:

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

通过在前面使用new来调用foo(..),我们构建了一个新的对象并这个新对象作为foo(..)调用的this。 new是函数调用可以绑定this的最后一种方式,我们称之为 new绑定(new binding)。

3.5 优先级

  • new绑定
  • 显示绑定
  • 隐式绑定
  • 默认绑定(严格模式下会绑定到undefined)

4. 箭头函数

箭头函数并非使用function关键字进行定义,而是通过所谓的“大箭头”操作符:=>,所以不会使用上面所讲解的this四种标准规范,箭头函数从封闭它的(function或global)作用域采用this绑定,即箭头函数会继承自外层函数调用的this绑定。

执行 fruit.call(apple) 时,箭头函数this已被绑定,无法再次被修改。

function fruit(){
    return () => {
        console.log(this.name);
    }
}
var apple = {
    name: '苹果'
}
var banana = {
    name: '香蕉'
}
var fruitCall = fruit.call(apple);
fruitCall.call(banana); // 苹果

5. 小结

this是JavaScript的一个关键字,this不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this绑定和函数声明的位置无关,反而和函数被调用的方式有关。为执行中的函数判定this绑定需要找到这个函数的直接调用点。找到之后,4种规则将会以 这个 优先顺序施用于调用点:

  • 被new调用?使用新构建的对象。
  • 被call或apply(或 bind)调用?使用指定的对象。
  • 被持有调用的环境对象调用?使用那个环境对象。
  • 默认:strict mode下是undefined,否则就是全局对

与这4种绑定规则不同,ES6的箭头方法使用词法作用域来决定this绑定,这意味着它们采用封闭他们的函数调用作为this绑定(无论它是什么)。它们实质上是ES6之前的self = this代码的语法替代品。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK