深入理解this

feng

什么是作用域?

几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个 值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做 不到非常有趣。
但是将变量引入程序会引起几个很有意思的问题,这些变量住在 哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?
这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。 这套规则被称为作用域。
简而言之,作用域是根据名称查找变量的一套规则。
(以上摘录自《你不知道的JS》)

什么是this?

JavaScript有两种作用域:词法(静态)作用域和动态作用域(即this)。
词法作用域即函数和变量的作用域在定义的时候就确定了(即声明的位置)。如:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 结果是 ???

答案是1. foo定义在全局对象,所以作用域即window, foo内部没有找到value,就往作用域链上面找,即window,window对象有声明value,则打印1.
注:浏览器的全局对象默认是window

在看一个例子(摘取《你不知道的JS》):

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() 显式传入一个上下文对象(context)。

function identify(context) {
    return context.name.toUpperCase(); 
} 
 
function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting ); 
}
var me = {
    name: "Kyle" 
}; 
 
var you = {  
    name: "Reader" 
}; 

identify(you); // READER 
speak(me); //Hello, I'm KYLE

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 提供了一种更优雅的方式来隐式“传递”一个对象引用。也就是动态作用域。this指的是函数运行时所在的环境(上下文对象)。

了解了作用域和this,接下来进入本文的主题:

如何确定函数的运行环境?

var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo() // 1 
foo() // 2
同样都是foo函数,为什么结果不一样呢?这种差异的原因,就在于函数体内部使用了this关键字。很多教科书会告诉你,this指的是函数运行时所在的环境。对于obj.foo()来说,foo运行在obj环境,所以this指向obj;对于foo()来说,foo运行在全局环境,所以this指向全局环境。所以,两者的运行结果不一样。

这种解释没错,但是教科书往往不告诉你,为什么会这样?也就是说,函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()就是在obj环境执行,而一旦var foo = obj.foofoo()就变成在全局环境执行?

这篇文章从内存的数据结构以及函数如何存储方法进行了解释。
下面简要总结下:
JavaScript 中函数也是对象。上面代码中foo函数有一块独立的内存来保存函数,obj.foo保存的是这块内存的地址。由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行(即被任意对象调用)
比如obj.foo() foo是在obj内部的指向函数的属性,并通过 obj.foo 间接引用了函数,所以函数调用位置上下文对象是obj,所以,this 指代的是 foo函数的调用者obj,打印1
foo() 是直接使用不带任何修饰的函数引用进行调用, 相当于独立调用,而非由其他对象所调用,这种情况在非严格环境下,this会默认绑定到全局对象window, 所以打印2
注:严格环境下会是undefined

下面再来看几个例子:
例一:
image.png
通过上述解释,funs通过obj.funs()调用,所以funs内this是obj,虽然fun2定义在funs内部,但是上面说过:this指的是函数运行时所在的环境(上下文对象)。 和定义在哪里无关。funs() 通过函数引用直接调用,所以会使用默认绑定到window
上述例子我们知道了this绑定的两种情况:

  1. 隐式绑定:由指定对象间接调用函数,绑定到指定对象
  2. 默认绑定:非严格模式下window,严格模式下undefined

那么如何让例一中fun2()的this也绑定到obj呢?
改造下:

let obj = {
    funs() {
        let fun2 = function() { console.log(this) } //obj
        fun2.call(this)
    }
}
obj.funs()

通过call, apply, bind等方法可以显示指定this绑定到哪个对象。这便是this的显示绑定。

new

当我们使用new来调用函数的时候,此时函数被当作构造函数来处理,具体new的实现可以参考这篇文章,构造函数内的this绑定到新生成的对象。

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

this绑定的四种规则

至此,上述内容可以总结出四种规则来确定this的值:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()

以上出自《你不知道的JS》

this词法

我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。

将上面的例一改造下:

let obj = {
    funs() {
        let fun2 = () => { console.log(this) } //obj
        fun2()
    }
}
obj.funs()

箭头函数使用词法作用域(在定义时确定),fun2定义在函数funs内,所以函数funs环境决定了fun2的环境。
obj.funs()调用后funs的环境是obj, 所以fun2的环境也是obj, 后面调用时打印出obj。
注:箭头函数this值无法被修改

绑定例外

在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用 的可能是默认绑定规则。

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

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值 在调用时会被忽略,实际应用的是默认绑定规则。

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。
如示例3 赋值表达式 foo.bar = foo.bar 的返回值是目标函数的引用,函数实际是直接使用不带任何修饰的(匿名)函数引用进行调用, 类似与通过 foo() 调用(注:这里若调用foo()会报not function错误哦,因为全局未声明) 而不是 foo.bar()调用 。根据我们之前说过的,这里会应用默认绑定。示例4,5 分别使用 逻辑运算和逗号运算符返回了目标函数的引用,因此结果同示例3。

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是 函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。

这里是关于this的测试题

参考来源

书籍《你不知道的JavaScript》
JavaScript 的 this 原理 -- 阮一峰
JavaScript深入之从ECMAScript规范解读this -- 冴羽
JavaScript深入之new的模拟实现 -- 冴羽

阅读 451
9 声望
0 粉丝
0 条评论
你知道吗?

9 声望
0 粉丝
宣传栏