It's all about
connecting the dots

node和浏览器环境下同一段代码输出结果不同

一、现象描述

今天被问到下面这段代码在浏览器和vscode里为啥执行结果不一样。我先是一愣,然后第一反应是是不是vsode里默认用了严格模式(use strict),然后浏览器里默认没用严格模式。事实上这事和是否严格模式没关系,vscode也不会将你的代码弄成严格模式。另外,如果将下面这些代码放在严格模式下,其实是会报错的,因为匿名函数里的this在严格模式下是undefined,所以this.number这里就会报错了。

var number = 2;
var obj = {
  number: 4,
  fn1: (function () {
    this.number *= 2;
    number *= 2;
    var number = 3;
    return function () {
      this.number *= 2;
      number *= 3;
      console.log('1', number);
    }
  })()
}
var fn1 = obj.fn1;
console.log('2', number);
fn1();
obj.fn1();
console.log('3', number);
console.log('4', obj.number);

当我们用装了Code Runner插件的vscode来执行上述代码时,Code Runner其实是调用了node来解析对应的程序,所以朋友的问题其实是node环境和浏览器环境下的差异导致的。

先说结果,上述代码在浏览器环境下的执行结果如下图所示:

但是在node环境下的执行结果则如下图所示:

二、结果分析

知道或者怀疑是环境差异导致的话,一般来说你已经离正确答案之差一个搜索操作了,因为显然这个问题并非很奇葩,应该在我们之前已经有很多人遇到过了。不过这里想要知道实际的代码执行顺序还有个非常好用的方法,就是直接断点调试。对与浏览器环境,在代码的头部加一行debugger;,然后在浏览器控制台里执行之即可。对于node环境,如果是vscode,就直接点击菜单栏里的Run=>Start Debugging,其他IDE也有类似操作。

2.1、在浏览器下的执行过程

这个地方其实就是平时面试容易被问到的点了,而且这个题目很明显是个面试题,出题者问的其实也是浏览器下的执行结果(这时候你提到node下执行结果不一样的话,可显著加分)。下面说执行过程。

1、最先是由于var的声明会提升到最前端,所以先是执行了var number、var obj、var fn1三个声明。

2、然后执行number = 2;赋值语句,这时候window.number值为2。

3、然后执行obj的赋值。由于obj.fn1是一个IIFE(immediately-invoked function expression),所以赋值obj的时候,会执行这个自执行的匿名函数。在非严格模式下(严格模式下匿名函数里的this是undefined,这段代码是会报错的),匿名函数里的this是全局对象,在这里就是window对象,所以在执行this.number *= 2之后,window.number的值变成了2*2,即4。这个obj.fn1对应的自执行函数部分中的开头几行:

this.number *= 2;
number *= 2;
var number = 3;

实际等价于:

var number;
window.number *= 2;
number *= 2; // 此时number = number * 2 = undefined * 2 = NaN
number = 3; // number重新被赋值为3(之前为NaN)

这个IIFE里面最后return了一个匿名函数(注意这里形成了一个闭包,此时值为3个这个number只有在最后被return的匿名函数里才能读取/修改这个number的值),这个被return的匿名函数主体部分为:

this.number *= 2;
number *= 3;
console.log('1', number);

注意这里的number由于此时obj.fn1尚未被调用,所以忽略这里。

4、执行赋值语句fn1 = obj.fn1。

5、执行打印语句console.log(‘2’, number)。这里的number就是window.number,所以打印出来的number的值是4。

6、执行fn1,即window.fn1,即fn1是被window对象调用的,所以此时fn1函数内部的this指向的是window对象,所以执行this.number *= 2之后,window.number的值变成了4*2,即8。然后number *= 3会将上面形成的闭包里值为3的number的值更新为3*3,即9。所以console.log(‘1’, number)打印出来的number值为9。

7、执行obj.fn1。和上面执行fn1时非常相似,区别在于此时fn1是obj的方法,由obj对象调用,所以fn1函数体内部的this此时为obj对象。所以this.number *= 2等价于obj.number = obj.number * 2,即8。此时number *= 3会将闭包里值为9的number更新为27。

8、执行console.log(‘3’, number)。此时number值为window.number,即8。

9、执行console.log(‘4’, obj.number)。此时number值为8。

2.2、在node下的执行顺序

不讲了,就知道是有区别的就行了。看了和浏览器端的行为弄混了,面试都答不出来了。项目中要是写出来这种代码,不管结果对不对,都要算bug,因为完全没可读性科研。所以这个没有了解的必要。拒绝内卷,从我开始,不浪费时间在过渡内卷上。

好吧,出于好奇心还是看了下debug时变量的变化,当执行到下面的代码行时,左侧我们可以看到Local下有个number值为2:

然后在Global下可以看到也有个number,但是值为NaN:

所以和上面的浏览器环境下的分析思路其实并不冲突,匿名函数里的this还是指向全局变量。需要注意的是上图里第二行var number = 2;里的number可以理解为当前模块内的局部变量,并非全局变量,而截图里第6行通过this.number修改的是全局变量里的number,也就是二者互相没有关系。在Node环境下执行上述代码的实际效果等同于把上面的整个代码放到一个IIFE函数内部,然后在浏览器上执行的效果。如下图所示:

赞(3) 打赏
转载需注明来源并给出来源页链接:峰间的云 » node和浏览器环境下同一段代码输出结果不同

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏