前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JS入门难点解析7-this

JS入门难点解析7-this

作者头像
love丁酥酥
发布2018-08-27 15:35:41
7110
发布2018-08-27 15:35:41
举报
文章被收录于专栏:coding for lovecoding for love

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!)

(注2:更多内容请查看我的目录。)

1. 简介

老样子,我们列一下执行上下文的三大属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

this是一个非常容易让人混淆的概念。首先我们思考一下JS中为什么会有this。

2. this的作用

看一下如下代码:

代码语言:javascript
复制
function identify() {
    return this.name.toUpperCase();
}

function speak() {
    var greeting = "Hello, I'm " + identify.call(this);
    console.log(greeting);
}

var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是 KYLE 
speak.call( you ); // Hello, 我是 READER

这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。

如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。如下所示:

代码语言:javascript
复制
function identify(context) {
    return context.name.toUpperCase();
}

function speak(context) {
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}

identify(you); // READER
speak(me); //hello, 我是 KYLE

然而,this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。后面介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象有多重要。

3. this的两种错误解读

this常见的错误解读有两种,下面我们来仔细分析一下。

3.1 this指向自身

this,字面上的理解就是“这”,大家很容易将其解读为指向这个函数自身。看一下如下代码:

代码语言:javascript
复制
function foo(num) {
    console.log( "foo: " + num );
    // 记录 foo 被调用的次数
    this.count++; 
}
foo.count = 0;
var i;
for (i=0; i<10; i++) { 
    if (i > 5) {
        foo( i ); 
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次?
console.log( foo.count ); // 0

console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然是 0。显然从字面意思来理解 this 是错误的。其实,此处this.count++创建了一个全局变量count。执行count++以后count变量成了NaN。如果不相信,大家可以在最后一行尝试输出window.count。

这个例子说明this并不能单纯理解为指向这个函数本身。不过,既然this不是指向函数本身,我们在函数内部如何引用函数本身呢?主要有以下三个方法:

  1. 具名引用 例如:
代码语言:javascript
复制
function foo() {
  foo.count = 4; // foo指向它自身
}

该方法只能用于具名函数中。

  1. arguments. callee 例如:
代码语言:javascript
复制
setTimeout( function(){
  arguments.callee.count = 4;  // 匿名(没有名字的)函数无法指向自身
}, 10 );

但是这种写法已被废弃,不建议使用。

  1. this 刚才不是说this不是指向函数本身么?可是现在为什又说可以呢?别急,等看完这篇文章,你自然会有答案。

3.2 this指向其作用域

这是this最使人混淆的地方。需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。同时,this与作用域链也不相关。

看下面的代码:

代码语言:javascript
复制
function foo() { 
    var a = 2;
    this.bar(); 
}
function bar() { 
    console.log( this.a );
}
foo(); // undefined

这段代码本意是想,foo在全局定义,那么this就指向全局,this.bar就可以调用全局中定义的bar,而bar执行的时候呢正好是在foo的执行上下文,所以this指向foo。其实这里对this的两处解读都是错误的。首先,this.bar()能够运行完全是一种偶然,怎样的偶然呢?你使用的是非严格模式,你是在浏览器环境运行而不是在node运行,你是独立调用的foo而正好bar在全局声明。是不是很巧合呢?是不是有点迷糊,不要紧,继续往下看。第二点,代码视图在bar里面打印foo的变量,这里是完全错误的,因为bar在运行时,this也是指向了全局(非严格模式,下面我们的讨论都是基于运行于浏览器的非严格模式)。

3.3 this的真正解读

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

4. 绑定规则与函数调用方式

this在运行时进行绑定,绑定主要有四种规则,取决于绑定时候函数的调用方式。

4.1 默认绑定与独立调用(函数调用模式)

独立调用是指函数作为一个普通函数来调用。当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的。对于普通的函数调用来说,函数的返回值就是调用表达式的值。

使用函数调用模式调用函数时,非严格模式下,this被绑定到全局对象;在严格模式下,this是undefined。

以下是四种常见的独立调用场景。

  1. 普通独立调用
代码语言:javascript
复制
function foo(){
    console.log(this === window);
}
foo(); //true
  1. 被嵌套的函数独立调用
代码语言:javascript
复制
//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0

3.IIFE(立即执行函数)

代码语言:javascript
复制
var a = 0;
function foo(){
    (function test(){
        console.log(this.a);
    })()
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0

其实立即执行函数可以理解为立即赋值并独立调用。上面代码其实和下面效果一样:

代码语言:javascript
复制
var a = 0;
function foo(){
    var temp = (function test(){
        console.log(this.a);
    });
    temp();  // 独立调用
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo();//0
  1. 闭包
代码语言:javascript
复制
var a = 0;
function foo(){
    function test(){
        console.log(this.a);
    }
    return test;
};
var obj = {
    a : 2,
    foo:foo
}
obj.foo()();//0

4.2. 隐式绑定和方法调用(方法调用模式)

当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被直接对象所调用时,this会被隐式绑定到该对象。如果调用表达式包含一个提取属性的动作,那么它就是被当做一个方法来调用。要记住,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

代码语言:javascript
复制
function foo(){
    console.log(this.a);
};
var obj1 = {
    a:1,
    foo:foo,
    obj2:{
        a:2,
        foo:foo
    }
}

//foo()函数的直接对象是obj1,this隐式绑定到obj1
obj1.foo();//1

//foo()函数的直接对象是obj2,this隐式绑定到obj2
obj1.obj2.foo();//2

对于隐式绑定,是最容易出现错误的地方,也是面试出陷阱题最多的地方。因为很容易出现所谓的隐式丢失。隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。我们来看一下哪些情况会出现隐式丢失。

  1. 函数别名
代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();//0

等价于

代码语言:javascript
复制
var a = 0;
var bar = function foo(){
    console.log(this.a);
}
bar();//0

其实,要理解一点。就是函数在进入函数执行上下文时才会进行this绑定,也就是这个函数调用以后才会进行this绑定。而此处仅仅是做了引用赋值,然后进行了bar的独立调用。后面出现的所谓隐式丢失,其实都可以用这个道理去解释。

  1. 参数传递
代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo);//0

等价于

代码语言:javascript
复制
//等价于
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});
  1. 内置函数
代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
setTimeout(obj.foo,100);//0

等价于

代码语言:javascript
复制
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);//0
  1. 间接引用
代码语言:javascript
复制
function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
(p.foo = o.foo)(); // 2

不等价于

代码语言:javascript
复制
function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
p.foo = o.foo;
p.foo();//4
  1. 其他情况 在javascript引擎内部,obj和obj.foo储存在两个内存地址,简称为M1和M2。只有obj.foo()这样调用时,是从M1调用M2,因此this指向obj。但是,下面三种情况,都是直接取出M2进行运算,然后就在全局环境执行运算结果(还是M2),因此this指向全局环境。
代码语言:javascript
复制
var a = 0;
var obj = {
    a : 2,
    foo:foo
};
function foo() {
    console.log( this.a );
};

(obj.foo = obj.foo)(); // 0

(false || obj.foo)(); // 0

(1, obj.foo)(); // 0

// 直接加括号并不会有造成隐式丢失
(obj.foo)(); // 2

总结:其实,隐式绑定只有在直接进行对象方法调用时才会出现,就是读取到属性方法以后直接在后面加括号调用,如下:

代码语言:javascript
复制
obj.foo();

如果在调用前经过了任何运算,比如“=”,“,”“||”等运算(注意"()"并不是运算符),其实都是执行了一个隐式的赋值引用,然后对被隐式赋值的函数进行了直接调用,如下:

代码语言:javascript
复制
(obj2.foo = obj.foo)();
(obj.foo = obj.foo)();
(false || obj.foo)();
(1, obj.foo)();
...

等价于

代码语言:javascript
复制
var temp = (obj2.foo = obj.foo);temp();
var temp = (obj.foo = obj.foo);temp();
var temp = (false || obj.foo);temp();
var temp = (1, obj.foo);temp();
...

4.3 显式绑定与间接调用(间接调用模式)

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

可以通过call()、apply()、bind()方法把对象绑定到this上,这种做法叫做显式绑定。对于被调用的函数来说,叫做间接调用。

代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
foo(); // 0
foo.call(obj); // 2

obj并没有指向函数foo的属性,但却可以间接调用foo。这时候我们可以回答文章开头提出的问题了。另一种指向自身的方式,使用this进行显示绑定。

代码语言:javascript
复制
function foo(){
    console.log(this);
}
foo();  // Window
foo.call(foo);  // f foo

不过,这种普通的显式绑定无法解决前面提到的隐式丢失问题。以前面所举的函数别名导致隐式丢失的代码为例。

代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();//0

改成如下显示绑定:

代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
var obj2 = {
    a : 3,
}
var bar = obj.foo;
bar();//0
bar.call(obj);//2
bar.call(obj2);//3

说明,call传入的对象改变时,隐式绑定的对象也发生了改变,this不再绑定foo的直接拥有者obj,发生了隐式丢失。那么如何防止这种隐式丢失呢?只要想办法让this始终指向其属性拥有者即可。当然我们也可以让this指向任何事先设定的对象,做到一种强制的绑定,也就是所谓的硬绑定。

代码语言:javascript
复制
var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
var obj2 = {
    a : 3,
}
var bar = function () {
  foo.call(obj);
};
bar();//2
bar.call(obj);//2
bar.call(obj2);//2

不管给call传入什么,最后this实际绑定的对象都是预先指定的obj。

JavaScript中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()

代码语言:javascript
复制
var id = 'window';
function foo(el){
    console.log(el,this.id);
}
var obj = {
    id: 'fn'
};
[1,2,3].forEach(foo); // 1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj); // 1 "fn" 2 "fn" 3 "fn"

4.4 new绑定和构造函数调用(构造函数调用模式)

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。要注意以下几点:

  1. 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
代码语言:javascript
复制
function fn(){
    this.a = 2;
}
var test = new fn();
console.log(test);  // {a:2}
  1. 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
代码语言:javascript
复制
function fn(){
    this.a = 2;
    return 1;
}
var test = new fn();
console.log(test); // {a:2}
  1. 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。
代码语言:javascript
复制
var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);  // {a:1}
  1. 尽管有时候构造函数看起来像一个方法调用,它依然会使用这个新对象作为this。也就是说,在表达式new o.m()中,this并不是o。
代码语言:javascript
复制
var o = {
    m: function(){
        this.a = 1;
        return this;
    }
}
var obj = new o.m();
console.log(obj, obj === o);  // {a:1} , false
console.log(obj.a);  // 1
console.log(o.a);  // undefined
console.log(o.m.a);  // undefined
console.log(obj.constructor === o.m);  // true

5. this绑定优先级

现在我们已经了解了函数调用中 this 绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?为了 解决这个问题就必须给这些规则设定优先级,这就是我们接下来要介绍的内容。

毫无疑问,默认绑定的优先级是四条规则中最低的,所以我们可以先不考虑它。现在我们将其与另外三种规则互相比较。

5.1 显式绑定 vs 隐式绑定

代码语言:javascript
复制
function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
//在该语句中,显式绑定call(obj2)和隐式绑定obj1.foo同时出现,最终结果为3,说明被绑定到了obj2中
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

可以看到,显式绑定优于隐式绑定

5.2 new绑定 vs 隐式绑定

代码语言:javascript
复制
function foo(something) {
    this.a = something;
}
var obj1 = {foo: foo};
var obj2 = {};

obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call(obj2,3);
console.log( obj2.a ); // 3

//在下列代码中,隐式绑定obj1.foo和new绑定同时出现。最终obj1.a结果是2,而bar.a结果是4,说明this被绑定到new创建的新对象上
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

可以看到,new绑定优先于隐式绑定

5.3 new绑定 vs 显式绑定

代码语言:javascript
复制
function foo(something) {
    this.a = something;
}
var obj1 = {};

// 先将obj1绑定到foo函数中,此时this值为obj1
var bar = foo.bind( obj1 );
bar( 2 );
console.log(obj1.a); // 2
// 通过new绑定,此时this值为baz
var baz = new bar( 3 );
console.log( obj1.a ); // 2
// 说明使用new绑定时,在bar函数内,无论this指向obj1有没有生效,最终this都指向实例baz
console.log( baz.a ); // 3

6. 总结

关于this的绑定,可以按照如下顺序判定:

  1. 是否是new绑定?如果是,this绑定的是新创建的实例对象
代码语言:javascript
复制
var bar = new foo();   // 绑定bar
  1. 是否是显式绑定?如果是,this绑定的是指定的对象
代码语言:javascript
复制
var bar = foo.call(obj2);  // 绑定obj2
  1. 是否是隐式绑定?如果是,this绑定的是调用的对象
代码语言:javascript
复制
var bar = obj1.foo();   // 绑定obj1
  1. 如果都不是,则使用默认绑定
代码语言:javascript
复制
var bar = foo();  // 绑定到全局对象(非严格模式)或者undefined(严格模式)

参考

深入理解this机制系列第一篇——this的4种绑定规则

JavaScript深入之从ECMAScript规范解读this

深入理解javascript函数系列第一篇——函数概述

深入理解this机制系列第二篇——this绑定优先级

BOOK-《你不知道的JavaScript》 第2部分

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.01.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简介
  • 2. this的作用
  • 3. this的两种错误解读
    • 3.1 this指向自身
      • 3.2 this指向其作用域
      • 3.3 this的真正解读
      • 4. 绑定规则与函数调用方式
        • 4.1 默认绑定与独立调用(函数调用模式)
          • 4.2. 隐式绑定和方法调用(方法调用模式)
            • 4.3 显式绑定与间接调用(间接调用模式)
              • 4.4 new绑定和构造函数调用(构造函数调用模式)
              • 5. this绑定优先级
                • 5.1 显式绑定 vs 隐式绑定
                  • 5.2 new绑定 vs 隐式绑定
                    • 5.3 new绑定 vs 显式绑定
                    • 6. 总结
                    • 参考
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档