请问在浏览器和nodejs下这段代码的结果为什么不一样?
发布于 3 年前 作者 Or0chimaru 3210 次浏览 来自 问答

Node版本是 v14.15.5

这段代码写成 .js 文件用node运行没有打印出2,但是在node命令行中逐条输入运行是可以打印出2的,而在浏览器中两种方式都可以,不论是写成文件用html引入还是在控制台中。为什么写成 .js 文件用node运行没有打印出2呢?

SharedScreenshot.jpg

6 回复

首先,我们可以明确的是这种情况下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 控制台

不重要,不说了

回到顶部