Node版本是 v14.15.5
这段代码写成 .js 文件用node运行没有打印出2,但是在node命令行中逐条输入运行是可以打印出2的,而在浏览器中两种方式都可以,不论是写成文件用html引入还是在控制台中。为什么写成 .js 文件用node运行没有打印出2呢?
首先,我们可以明确的是这种情况下foo函数里面的this一定是指向全局对象的,在node里是global,在浏览器中是window 我们发现在node里面输出的是undefined,这就说明bar不在全局对象上 为什么会这样呢?回想一下var声明的变量只有全局和函数作用域,如果我们直接写成bar=2;没有var修饰那将一定是在全局作用域,改成这种写法后无论是node还是浏览器运行结果都是2了。 再深挖一下,如果我们在最后添加一行console.log(this.bar);会发现浏览器还是输出2,node又输出undefined了,这说明在这个地方node环境this并不是指向全局对象,因为我们已经证明全局对象上存在bar了而这里没有,这又是为什么?这时候this指向谁?
function(module, exports) {
// 你的代码
}
因为node中一个js文件就是一个模块,实际上你的代码在node中执行时至少是放在一个像这样的函数中的,这就很好的解释了为什么在node运行时var修饰的bar没有出现在全局对象上 如果你的代码写成这样:
function test() {
var obj = {
foo: function () { console.log(this.bar) },
bar: 1,
};
var foo = obj.foo;
bar = 2;
foo();
console.log(this.bar);
}
new test();
在node和浏览器的运行结果就是相同的了,最后那个奇怪的this指向问题也得到了解答 最后,请各位大佬不要拿这种问题来做面试题!!!
@zengming00 非常感谢,谢谢前辈解惑~
我根据 语言标准 来做一个补充
var obj = {
foo: function () { console.log(this.bar) },
bar: 1,
};
var foo = obj.foo;
foo = obj.foo
对应的执行流程通过 赋值操作符(AssignmentOperator) 来解释
赋值操作符 =
规定引擎要对右侧的赋值表达式进行求值。求值的结果通过一个引擎内部实现使用的类型 Reference 来表示,需要补充的是 Reference 并不一定在引擎内部存在实体映射,因为它只是语言标准中为了方便描述而提出的数据结构
Reference 有 base 和 referenced name 两个字段,针对例子 obj.foo
的执行结果就是 Reference{base: obj, referencedName: 'foo'}
注意接下来的步骤,赋值操作符继续规定,要求对上一步的求值结果进一步做 GetValue(rref)
操作
GetValue(Reference{base: obj, referencedName: 'foo'})
的结果就是返回了一个函数对象(不是 Reference 了)
再看 foo()
对应的描述是 调用表达式(CallExpression) 的执行过程,调用表达式的组成为两部分:
CallExpression : MemberExpression Arguments
MemberExpression
就对应例子里 foo()
中的 foo
,标准规定要对 MemberExpression 进行求值,除了求值以外,这一步要需要准备一个变量 thisValue
:
- 如果
MemberExpression
的求值结果 v 是 Reference 类型的话,那么就让thisValue
等于GetThisValue(v)
- 如果不是 Reference 类型则
thisValue
就是undefined
。对应这里的例子,此时thisValue
就是undefined
了
当然 Arguments 也是要求值的,但是这里就跳过了。最后的完整的函数调用形式,可以看成是这样:
F.[[Call]](V, argumentsList)
没错,按标准的描述,就是 foo.call(thisValue, argumentsList)
的意思,那换成例子到这里就是 foo.call(undefined)
继续 foo.call
对应的是 [[Call]] ( thisArgument, argumentsList),可以理解成引擎内部的一个方法,它内部又会调用 OrdinaryCallBindThis 用于将 thisArgument
绑定到当前的执行环境
最后要说道 this.bar
对应的是 Property Accessors 就是那个 .
的作用,它要求对左边的表达式求值,也就是这里的 this
表达式
this
表达式的求值对应的是 ResolveThisBinding,它要从当前环境开始(包括当前环境)不断看 outer 环境是否存在 this 绑定,返回第一个有 this 绑定的环境,然后取那个环境中的 this 绑定作为求值结果。对于例子来说,只有遍历到顶层的环境才会返回,一方面是因为直到顶层都没有 this 绑定,另一方面因为顶层环境总是有 this 绑定
所以例子在浏览器和 node 表现差异就在于 var bar = 2
有没有被绑定到顶层环境
一时看不懂也没事,以后想深入的话再回头看,或者也可以直接看上面给出的链接中的原文
@hsiaosiyuan0 感谢大佬,没想到一个this居然水这么深
问题一:this 指向
记住:
- this 指向“方法的调用者”
- “找不到”调用者时,this 就指向“根对象”(也就是浏览器里的 window 对象或node 里的 global 对象)
比如:
let a = { name: 'haha' }
let b = { name: 'heihei' }
function f(){ console.log(this.name) }
f() // 啥也没有
a.f = f
a.f() // 2. 'haha'
b.f = f
b.f() // 3. 'heihei'
f() // 啥也没有
- 第二次时,使用 a 调用 f 函数,那么 this 就指向 a
- 第三次一样
- 第一次和最后一次,没用任何对象调用 f,所以指向 this 指向 window
这一点非常重要,也很简单。但往往被误以为很复杂,而且讲得也很复杂。但,很简单。
下面一点不怎么重要,但对你的问题的解答,还是有帮助的。
问题二:全局变量的归属
浏览器环境
假设有这段代码:
<html>
<script>
var bar = 2
console.log(window.bar) // 输出 2
</script>
</html>
记住:
- 浏览器里的全局变量,都被赋给 window 对象
所以,在浏览器中,你的代码:
- foo 被调用时,“没有”调用者,所以 this 指向根对象,也就是 window
- 你声明的 bar 全局变量,正好被赋给了 window 对象
一般人不会这么写代码,或者说,一般人不会使用全局变量
因为这样非常容易出错
但我们经常一不小心,就声明一个不起眼全局变量,这个只能用“仔细”来避免
所以,node 干脆不让你“一不小心”声明全局变量
node 环境
在浏览器里,你可以通过 var bar = 2
来声明一个全局变量(而且,必须在任何函数之外)
而在 node 里,你只能通过 global.bar
来声明(在任何地方都行)
所以你的 var bar = 2
不被赋给 global 变量
所以,foo 里的输出,啥也没有(undefined)
node 控制台
不重要,不说了