JavaScript 的 this 原理

作者: 阮一峰

日期: 2018年6月18日

珠峰培训

一、问题的由来

学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能有不一样的结果。


var obj = {
  foo: function () {}
};

var foo = obj.foo;

// 写法一
obj.foo()

// 写法二
foo()

上面代码中,虽然obj.foofoo指向同一个函数,但是执行结果可能不一样。请看下面的例子。


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

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

obj.foo() // 1
foo() // 2

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

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

本文就来解释 JavaScript 这样处理的原理。理解了这一点,你就会彻底理解this的作用。

二、内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。


var obj = { foo:  5 };

上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。


{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo属性的值保存在属性描述对象的value属性里面。

三、函数

这样的结构是很清晰的,问题在于属性的值可能是一个函数。


var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。


{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。


var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

四、环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。


var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。


var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的this.x就是指当前运行环境的x


var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

上面代码中,函数f在全局环境执行,this.x指向全局环境的x

obj环境执行,this.x指向obj.x

回到本文开头提出的问题,obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

(完)

优达学城

腾讯课堂

留言(47条)

文中讲到 “环境” ,我脑中浮现的是“调用者级级别”或者“调用者所在的栈层”

简单明了

上面代码中,函数f在全局环境执行,this.x指向全局环境的this。——>上面代码中,函数f在全局环境执行,this.x指向全局环境的x。

很喜欢前辈的文章,通俗易懂

Hello

有个小建议

这样 是不是更好一点?

引用焦某的发言:

Hello

有个小建议


这样 是不是更好一点?

没想到是纯html。。。。。

搜索按钮的onclick值改为
this.form.wd.value = this.form.origin.value + ' 阮一峰'


是不是稍微好点?

如果不这样还有其他奇怪的百度知道的提问加上你的名字乱入搜索结果。

对this的描述依然抽象, 感觉画了那么多diagram意义不大, 只保留第一部分就好了

对 this 的理解又进一步加深,感谢

@dc:

谢谢指出,已经改正。

简洁明了

分享一篇自己总结的有道笔记:
http://note.youdao.com/noteshare?id=b2fab3b044aa90033395df0c8c9ca3a4&sub=6DA85DF56143475588CA9E0F9690FCBE

其中主要的内容是这篇文章里的,是我读过的,关于this指向写的最简单的文章了:
http://davidshariff.com/blog/javascript-this-keyword/#first-article

其中这句话算是精华了吧:
Whenever a function is called, we must look at the immediate left side of the brackets / parentheses “()”. If on the left side of the parentheses we can see a reference, then the value of “this” passed to the function call is exactly of which that object belongs to, otherwise it is the global object.

谢谢阮的分享,作为一个初学者,令我在JavaScript上更容易理解。
不知道阮是否愿意分享一下计算机的独特见解,最近在了解图灵,自己也有一些对计算机重新的认识,不知道阮在这方面的理解是什么,可能有些冒昧,请谅解

通俗易懂,非常好

这什么都没有解释啊。用运行环境解释运行环境。还引出了属性描述对象,但后面又并没有解释任何相关性。

个人理解 this指的是调用者所处的栈的内存地址, 不知是否正确

可以这么理解么:

说白了就是看具体执行的那一句的作用域:

foo()就是在全局执行
bar.foo()就是在bar里执行

想起来去年学校里的Java考试在考察继承和多态的时候好像也有类似的知识点 = =

很不错,理解了函数和值在内存空间的存在形式。也理解了环境会因不同的赋值而有不同的结果。谢谢

我有个建议,希望老师您能在每个网页加一个回到顶部的按钮.这样方便阅读

望采纳,哈哈

Javascript 是一个文本作用域的语言, 就是说, 一个变量的作用域, 在写这个变量的时候确定. this 关键字是为了在 JS 中加入动态作用域而做的努力. 所谓动态作用域, 就是说变量的作用范围, 是根据函数调用的位置而定的. 从这个角度来理解 this, 就简单的多.

this 是 JS 中的动态作用域机制, 具体来说有四种, 优先级有低到高分别如下:
1. 默认的 this 绑定, 就是说 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格默认, this 就是全局变量 Node 环境中的 global, 浏览器环境中的 window.
2. 隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
3. 显示绑定: foo.call(obj, ...), foo.apply(obj,[...]), foo.bind(obj,...)
4. 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象.

详见: Your Donot Know JS : this & Object

@zhangjg:

对对,看了 Your Donot Know JS 这本书,恍然大悟。

这篇文章,学到了!

谢谢分享,通俗易懂,直观明了!赞一个!

引用zhangjg的发言:

Javascript 是一个文本作用域的语言, 就是说, 一个变量的作用域, 在写这个变量的时候确定. this 关键字是为了在 JS 中加入动态作用域而做的努力. 所谓动态作用域, 就是说变量的作用范围, 是根据函数调用的位置而定的.从这个角度来理解 this, 就简单的多.

this 是 JS 中的动态作用域机制, 具体来说有四种, 优先级有低到高分别如下:
1. 默认的 this 绑定, 就是说 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格默认, this 就是全局变量 Node 环境中的 global, 浏览器环境中的 window.
2. 隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
3. 显示绑定: foo.call(obj, ...), foo.apply(obj,[...]), foo.bind(obj,...)
4. 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象.

详见: Your Donot Know JS : this & Object

补充一下:严格模式下,默认的this就是undefined了

常来老师的blog,最中意这种简单明了又探究到底的文章,无限个赞!

感谢您的指导。感谢。看了几年,第一次回复!
谢谢~

感谢老师的指导。从入行就在看您的文章,谢谢~

谢谢阮老师分享

简单概括起来:this指向当前环境。
当js运行起来,会有一个大环境,直接使用函数,this会指向当前大环境。this.x就会在大环境中找x。
对象会带有小环境,所以,对象中的this.x会去寻找小环境中的x。
结论:有对象就是不一样~~~

阮老师,加油~

函数在定义的时候,解释器已经把当前的环境和函数一起打包保存起来了吧,所以才说函数有词法作用域啰。至于像 obj.foo() 这种对象调用函数的语法,无非就是 obj.foo(obj)的语法糖了吧, 如果是直接 foo() 这样函数调用,没有传this引用,函数体在执行时就只好从定义时打包的环境中找了,找不到就一层一层往上找,直到顶层发现this代表window对象,就用window啰。

引用展翼骐骥的发言:

这什么都没有解释啊。用运行环境解释运行环境。还引出了属性描述对象,但后面又并没有解释任何相关性。

我也是这样觉得,一开始就说“这种解释没错,但是教科书往往不告诉你,为什么会这样?”,然后讲了怎么存。就到了再后面“上面代码中,函数体里面的this.x就是指当前运行环境的x。”,“上面代码中,函数f在全局环境执行,this.x指向全局环境的x。”...

这不也是和“往往不告诉你,为什么会这样?”一样吗?根本没讲原理什么的。肯定通俗易懂啊。。。

我沒有看過 JavaScript 實作,如果作者是看過 JavaScript 實作寫了這篇,那表示 JavaScript 的 this 就是保存環境物件,而不是實例(Instance),而且 this 環境物件與實例在存取上具有相同的介面,這樣就不用區別兩者的不同。

也就難怪沒有 dot 運算子的時候,this 會是指向全域,因為這時一定是頂層的環境物件。

我在實現 ToyLang(https://github.com/JustinSDK/toy_lang) 時的作法不同,使用了函式本身保存環境物件,至於 this,單純就當成函式的隱含參數,若是透過 dot 運算子,在呼叫函式時會將 dot 左邊的物件當成是隱含的引數,因此,當函式不透過 dot 運算子時,是不會有 this 的,也就不會有指向全域的問題。

JavaScript 的作法在好處之一,是實現 Closure 時比較方便,因為都是在環境物件上查找,我的作法在實現 Closure 時,在目前環境物件上找不到變數時,要繼續在函式的環境物件上查找。

我的作法好處是,理解 this 簡單的多,它就是函式隱含的參數罷了,只有在使用 dot 運算時才會有這個參數,也沒有必須區別是否指向全域的問題 … XD

感谢老师的分享,对this的本质了解又加深一步了

您好,有一个疑问,foo中的value写的是指向函数的地址,那么指向箭头是不是应该由value:函数的地址 ---> function(){console.log(this.x)}

正巧看到一片类似的文章和大家分享下。

Understanding JavaScript Function Invocation and "this"
https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/

充分体现了JavaScript的博大精深,还是像这样剖析起来才容易理解

感谢分享,不过老师你在上面引用描述对象的时候为什么要用两个[[]]嵌套使用

结合你的图,附上作用域链的思想应该可以理解了!

啊,我明白了,感谢阮大

>this指向所在环境或者调用它的对象。
这是最常见的解释,但是好像并不准确,因为它并不能解释下面的例子:
```
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}
console.log((false || foo.bar)()); // 1
```

感觉什么都没有解释

没想到2018年了阮老师还在讲述this的知识
只可惜老师讲得还是和我之前所了解得并无太大差别

@Lasy:

个人看法:
`(false || foo.bar)()`,这条执行语句可以分步理解。
let step_1 = foo.bar; // 把 foo.bar 方法赋值给 step_1;
let step_2 = step_1(); // 此时 step_1 作为一个普通函数执行,this 指向 window

总结:由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。JavaScript 允许在函数体内部,引用当前环境的其他变量。而为了获得当前的运行环境,所以this就出现了,因此函数里面的this函数当前的运行环境。开头提出的问题,obj.foo()是通过obj找到foo(这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性),所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

一句话:this 总是指向调用它所在的函数的那个对象

一直没想明白的是,为什么函数的设计中,要使用隐式的this,如果函数中调用非参数的变量的时候,显示使用this会不会使开发者能够更加清晰的知道函数引用的变量的上下文是哪个

当前所运行的环境,一句话点醒!!

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接