一道JS面试题所引起的"血案",透过现象寻本质,再从本质看现象

以为本人写的不算很烂的话,能够登陆关注一下个人GitHub博客,博客会坚持写下去。javascript

今天同窗去面试,作了两道面试题,所有作错了,发过来给我看,我一眼就看出来了,由于这种题我作过,至于为何结果是那样,我也以前没有深究过,他问我为何,我也是一脸的懵逼,不能从根源上解释问题的缘由,因此并不能彻底让他信服。今天就借着这个机会深扒一下,若是没有耐心能够点击右上角,以看小说的心态看技术文章,蜻蜓点水,不加思考,这样的量变并不能带来质的改变。花上10+分钟认真阅读我相信你会受益不浅,没收获你买把武昌火车站同款菜刀砍我?。由于我是写完这篇博客再回头写这段话的,在写的过程当中也学到了不少,因此在此分享一下共同窗习。html

登高自卑,与君共勉。前端

下面一块儿看看这道题,同窗微信发给我截图:
java

若是看的不太清楚,我把代码敲一遍,给你们看看:git

var name = "jay"; //一看这二逼就是周杰伦的死忠粉
var person = {
    name: "kang",
    pro: {
        name: "Michael",
        getName: function() {
            return this.name;
        }
    }
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());

这里我就不卖关子了,很多童鞋也应该遇到过作过相似的题目,就是考察this,咱们先看看答案:github

console.log(person.pro.getName());//Michael
console.log(pepole());//jay

第一个很简单,this就是指向person.pro的引用,那么this.name就是person.pro.name,因而第一个就是输出Michael,再来看看第二个就蹊跷了,和第一个明明是同样的方法,为何输出的结果是jay呢?面试

既然咱们知道结果是jay了,反着推理一步步来,不难推出调用people()这个方法时候的this.name就至关于和var name = "jay",var声明的全局变量和全局环境下的this的变量有什么联系呢?;那么这个this究竟是什么,总得是一个具体东西吧?数组

咱们一步步分析,this.name这个this有一个name属性,很明显就是一个对象,那具体是什么对象呢?this的指向是在函数被调用的时候肯定的,因而有人说就是Window对象,没错是没错,确实是Window对象,而后var name声明的全局变量namewindow.name是相同的做用;可是你只只知其然,而不知其因此然,学深一门语言就是要有刨根问底的精神,打破砂锅问到底,知其然还要知其因此然浏览器

咱们就先验证一下,那个this究竟是不是window对象吧。咱们把代码稍微调整一下,输出this微信

var name = "jay"; //一看这二逼就是周杰伦的死忠粉
var person = {
    name: "kang",
    pro: {
        name: "Michael",
        getName: function() {
            console.log(this);
            return this.name;
        }
    }
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());

看看控制台输出,确实没错就是window对象。

再来看看var name声明的name和window.name是否相等呢?

var name;
console.log(name===window.name)

确实是同样的,类型和值没有任何的不一样。

好滴,那么你说this就是window对象,至于为何是这样你也不清楚,是否永远是这样呢?咱们看看这段代码输出又会是咋样呢?

'use strict';
var name = "jay"; //一看这二逼就是周杰伦的死忠粉
var person = {
    name: "kang",
    pro: {
        name: "Michael",
        getName: function() {
            console.log(this);
            return this.name;
        }
    }
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());

还会是跟上面同样的结果吗?咱们拭目以待.

看到结果没:Cannot read property 'name' of undefined,这是什么意思想必你们已经很清楚了,此时的this成了undefined了,undefined固然也就没有name这个属性,因此浏览器报错了。那么为何会这样呢?

一样换种写法再来看看这段代码输出什么呢?

var name = "jay";
var person = {
    name : "kang",
    getName : function(){
     return function(){
        return this.name;
     };
    }
};
console.log(person.getName()());

控制台本身输出一下看看,我想此时你的心情必定是这样的:

在弄明白这些问题以前,咱们先弄清楚全局环境下的thisvar声明的全局变量window对象之间的联系与区别:
先看四个简单的例子对比,均在js非严格模式测试,也就是没有声明'use strict':
demo1:

var name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)

demo2:

name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)

demo3:

window.name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)

demo4:

this.name="jawil";
console.log(name);
console.log(window.name)
console.log(this.name)

其实这四个demo是一个意思,输出的结果没有任何差异,为何没有差异呢?由于他们在同一个环境,也就是全局环境下:
咱们换一种在不一样的环境下执行这段代码看一看结果:
demo5:

var name="jawil";
var test={
    name:'jay',
    getName:function(){
    console.log(name);
    console.log(window.name)
    console.log(this.name)
    }
}
test.getName();

最后结果一次输出为:

console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jay

由于此处的this再也不指向全局对象了,因此结果确定不一样,咱们先来看看全局对象全局环境下的this,暂不考虑其余环境下的this

那么又有人会问什么是全局环境,什么又是全局对象,全局对象该怎么理解?

题外话

其实咱们看技术文章,总以为似懂非懂,只知其一;不知其二,不是看不懂代码,而是由于不少时候咱们对一些概念没有比较深刻的了解,可是也没有去认真继续下去考究,这也不能怪咱们,毕竟开发时候不太深刻这些概念对咱们业务也没啥影响,可是我发现我本身写东西时候,不把概念说清楚,总不能让人信服和完全明白你讲的是什么玩意,我想写博客最大的好处可让本身进一步提升,更深层次的理解你所学过的东西,你讲的别人都看不懂,你确认你真的懂了吗?

说到全局环境,咱们就会牵扯到另外一个概念那就是执行环境和函数的做用域

既然扯到这么深,就顺便扯扯执行环境和做用域,这些都是js这门语言的重点和难点,没有必定的沉淀很难去深刻探讨这些东西的.

函数的每次调用都有与之紧密相关的做用域和执行环境。从根本上来讲,做用域是基于函数的,而执行环境是基于对象的(例如:全局执行环境即全局对象window)。

咱们仍是先说一说全局对象吧,由于全局执行环境是基于全局对象的。

JavaScript 全局对象

全局属性和函数可用于全部内建的 JavaScript 对象。

全局对象描述

  1. 全局对象是预约义的对象,做为 JavaScript 的全局函数和全局属性的占位符。经过使用全局对象,能够访问全部其余全部预约义的对象、函数和属性。全局对象不是任何对象的属性,因此它没有名称。

  2. 在顶层 JavaScript 代码中,能够用关键字 this 引用全局对象。但一般没必要用这种方式引用全局对象,由于全局对象是做用域链的头,这意味着全部非限定性的变量和函数名都会做为该对象的属性来查询。例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是做用域链的头,还意味着在顶层 JavaScript 代码中声明的全部变量都将成为全局对象的属性。

  3. 全局对象只是一个对象,而不是类。既没有构造函数,也没法实例化一个新的全局对象。

  4. 在 JavaScript 代码嵌入一个特殊环境中时,全局对象一般具备环境特定的属性。实际上,ECMAScript 标准没有规定全局对象的类型,JavaScript 的实现或嵌入的 JavaScript 均可以把任意类型的对象做为全局对象,只要该对象定义了这里列出的基本属性和函数。例如,在容许经过 LiveConnect 或相关的技术来脚本化 Java 的 JavaScript 实现中,全局对象被赋予了这里列出的 java 和 Package 属性以及 getClass() 方法。而在客户端 JavaScript 中,全局对象就是 Window 对象,表示容许 JavaScript 代码的 Web 浏览器窗口。

例子

在 JavaScript 核心语言中,全局对象的预约义属性都是不可枚举的,全部能够用 for/in 循环列出全部隐式或显式声明的全局变量,以下所示:
上一篇博客我就讲到遍历对象属性的三种方法:

for-in循环、Object.keys()以及Object.getOwnPropertyNames()不一样的区别,想要了解能够细看我这篇博客:传送门

var variables = "";

for (var name in this)
{
variables += name + "<br />";
}

document.write(variables);

再回过头来谈谈执行环境和函数的做用域

一开始要明白的

  • 首先,咱们要知道执行环境和做用域是两个彻底不一样的概念。

  • 函数的每次调用都有与之紧密相关的做用域和执行环境。从根本上来讲,做用域是基于函数类型的(固然函数也是对象,这里咱们细分一下),而执行环境是基于对象类型的(例如:全局执行环境即window对象)。

  • 换句话说,做用域涉及到所被调用函数中的变量访问,而且不一样的调用场景是不同的。执行环境始终是this关键字的值,它是拥有当前所执行代码的对象的引用。每一个执行环境都有一个与之关联的变量对象,环境中定义的全部变量和函数都保存在这个对象中。虽然咱们编写的代码没法访问这个对象,但解析器在处理数据时会在后台使用它。

一些概念

1. 执行环境(也称执行上下文–execution context)

首先来讲说js中的执行环境,所谓执行环境(有时也称环境)它是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其余数据 ,决定了它们各自的行为。而每一个执行环境都有一个与之相关的变量对象,环境中定义的全部变量和函数都保存在这个对象中。

当JavaScript解释器初始化执行代码时,它首先默认进入全局执行环境,今后刻开始,函数的每次调用都会建立一个新的执行环境。

每一个函数都有本身的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中(execution stack)。在函数执行完后,栈将其环境弹出,把控制权返回给以前的执行环境。ECMAScript程序中的执行流正是由这个便利的机制控制着。执行环境能够分为建立和执行两个阶段。在建立阶段,解析器首先会建立一个变量对象(variable object,也称为活动对象activation object),它由定义在执行环境中的变量、函数声明、和参数组成。在这个阶段,做用域链会被初始化,this的值也会被最终肯定。在执行阶段,代码被解释执行。

1.1可执行的JavaScript代码分三种类型:
  1. Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。

  2. Eval Code,即便用eval()函数动态执行的JS代码。

  3. Function Code,即用户自定义函数中的函数体JS代码。

不一样类型的JavaScript代码具备不一样的Execution Context

Demo:

<script type="text/javascript">
    function Fn1(){
        function Fn2(){
            alert(document.body.tagName);//BODY
            //other code...
        }
        Fn2();
    }
    Fn1();
    //code here
</script>


特别说明:图片来自于笨蛋的座右铭博客

1.2执行环境小结

当javascript代码被浏览器载入后,默认最早进入的是一个全局执行环境。当在全局执行环境中调用执行一个函数时,程序流就进入该被调用函数内,此时JS引擎就会为该函数建立一个新的执行环境,而且将其压入到执行环境堆栈的顶部。浏览器老是执行当前在堆栈顶部的执行环境,一旦执行完毕,该执行环境就会从堆栈顶部被弹出,而后,进入其下的执行环境执行代码。这样,堆栈中的执行环境就会被依次执行而且弹出堆栈,直到回到全局执行环境。
此外还要注意一下几点:

  • 单线程

  • 同步执行

  • 惟一的全局执行环境

  • 局部执行环境的个数没有限制

  • 每次某个函数被调用,就会有个新的局部执行环境为其建立,即便是屡次调用的自身函数(即一个函数被调用屡次,也会建立多个不一样的局部执行环境)。

2. 做用域(scope)

当代码在一个环境中执行时,会建立变量对象的一个做用域链(scope chain。做用域链的用途是保证对执行环境有权访问的全部变量和函数的有序访问。

做用域链包含了执行环境栈中的每一个执行环境对应的变量对象.
经过做用域链,能够决定变量的访问和标识符的解析。
注意:全局执行环境的变量对象始终都是做用域链的最后一个对象。

在访问变量时,就必须存在一个可见性的问题(内层环境能够访问外层中的变量和函数,而外层环境不能访问内层的变量和函数)。更深刻的说,当访问一个变量或调用一个函数时,JavaScript引擎将不一样执行环境中的变量对象按照规则构建一个链表,在访问一个变量时,先在链表的第一个变量对象上查找,若是没有找到则继续在第二个变量对象上查找,直到搜索到全局执行环境的变量对象即window对象。这也就造成了Scope Chain的概念。


特别说明:图片来自于笨蛋的座右铭博客

做用域链图,清楚的表达了执行环境与做用域的关系(一一对应的关系),做用域与做用域之间的关系(链表结构,由上至下的关系)。
Demo:

var color = "blue";
function changeColor(){
  var anotherColor = "red";
  function swapColors(){
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
    // 这里能够访问color, anotherColor, 和 tempColor
  }
  // 这里能够访问color 和 anotherColor,可是不能访问 tempColor
  swapColors();
}
changeColor();
// 这里只能访问color
console.log("Color is now " + color);

上述代码一共包括三个执行环境:全局执行环境、changeColor()的局部执行环境、swapColors()的局部执行环境。

  • 全局环境有一个变量color和一个函数changecolor();

  • changecolor()函数的局部环境中具备一个anothercolor属性和一个swapcolors函数,固然,changecolor函数中能够访问自身以及它外围(即全局环境)中的变量;

  • swapcolor()函数的局部环境中具备一个变量tempcolor。在该函数内部能够访问上面的两个环境(changecolor和window)中的全部变量,由于那两个环境都是它的父执行环境。

上述代码的做用域链以下图所示:

从上图发现。内部环境能够经过做用域链访问全部的外部环境,可是外部环境不能访问内部环境中的任何变量和函数。
标识符解析(变量名或函数名搜索)是沿着做用域链一级一级地搜索标识符的过程。搜索过程始终从做用域链的前端开始,而后逐级地向后(全局执行环境)回溯,直到找到标识符为止。

3.执行环境与做用域的区别与联系

执行环境为全局执行环境和局部执行环境,局部执行环境是函数执行过程当中建立的。
做用域链是基于执行环境的变量对象的,由全部执行环境的变量对象(对于函数而言是活动对象,由于在函数执行环境中,变量对象是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO(变量对象)的角色。)共同组成。
当代码在一个环境中执行时,会建立变量对象的一个做用域链。做用域链的用途:是保证对执行环境有权访问的全部变量和函数的有序访问。做用域链的前端,始终都是当前执行的代码所在环境的变量对象。

4.小练习
<script type="text/javascript">
(function(){
    a= 5;
    console.log(window.a);//undefined
    var a = 1;//这里会发生变量声明提高
    console.log(a);//1
})();
</script>

window.a之因此是undefined,是由于var a = 1;发生了变量声明提高。至关于以下代码:

<script type="text/javascript">
(function(){
    var a;//a是局部变量
    a = 5;//这里局部环境中有a,就不会找全局中的
    console.log(window.a);//undefined
    a = 1;//这里会发生变量声明提高
    console.log(a);//1
})();
</script>

更多关于变量提高和执行上下文详细解说这里就很少少了,否则越扯越深,有兴趣能够看看这篇图解,浅显易懂:
前端基础进阶(二):执行上下文详细图解

相信你们看到这里,也很累了,可是也有收获,大概有了一些深入印象,对概念也有一些比较深刻的理解了。
这里我就稍微总结一下,上面讲了一些什么,对接下来的解析应该有很大的帮助。

**1. 浏览器的全局对象是window

  1. 全局执行环境即window对象所建立的,局部执行环境是函数执行过程当中建立的。

  2. 全局对象,能够访问全部其余全部预约义的对象、函数和属性。

  3. 当javascript代码被浏览器载入后,默认最早进入的是一个全局执行环境。

  4. 明白了执行上下文和做用域的一些概念,知道其中的运行机制和原理。**

咱们再回头看看这两个demo比较,咱们解释清楚这个demo执行的结果。
demo1:

var name="jawil";
console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jawill

demo2:

name="jawil";
console.log(name);//jawil
console.log(window.name)//jawil
console.log(this.name)//jawil

好,从例子看以看出,这两个name都是全局属性,未经过var声明的变量a和经过var声明的变量b,均可以经过this和window访问到.

咱们能够在控制台打印出windowd对象,发现name成了window对象的一个属性:

var name="jawil";
console.log(window);
name2="test";
console.log(window);

这是其实一个做用域和上下文的问题。在JavaScript中,this指向当前的上下文,而var定义的变量值在当前做用域中有效。JavaScript有两种做用域,全局做用域和局部做用域。局部做用域就是在一个函数里。var关键字使用来在当前做用于中建立局部变量的,而在浏览器中的JavaScript全局做用域中使用var语句时,会把申明的变量挂在window上,而全局做用域中的this上下文刚好指向的又是window,所以在全局做用域中var申明的变量和window上挂的变量,即this可访问的变量有间接的联系,但没有直接联系,更不是同样的。

上面的分析咱们知道了,全局变量,全局环境下的this,还有全局对象之间的关系了,具体总结一下就是:

**1. 全局环境的this会指向全局对象window,此时this===window;

  1. 全局变量会挂载在window对象下,会成为window下的一个属性。

  2. 若是你没有使用严格模式并给一个未声明的变量赋值的话,JS会自动建立一个全局变量。**

那么用var声明的全局变量赋值和未声明的全局变量赋值到底有什么不一样呢?这里再也不是理解理解这道面试题的重点,想深刻探究能够看看这篇文章:javascript中加var和不加var的区别 你真的懂吗.

该回头了,好累?,再来看看这道面试题:

![](http://odssgnnpf.bkt.clouddn.com/1440757221622060.jpg)
var name = "jay"; //一看这二逼就是周杰伦的死忠粉
var person = {
    name: "kang",
    pro: {
        name: "Michael",
        getName: function() {
            return this.name;
        }
    }
};
console.log(person.pro.getName());
var pepole = person.pro.getName;
console.log(pepole());

最后就成了为何person.pro.getName()的this是person.pro而pepole()的this成了window对象。这里咱们就要了解this的运行机制和原理。

在这里,咱们须要得出一个很是重要必定要牢记于心的结论,this的指向,是在函数被调用的时候肯定的。也就是执行上下文被建立时肯定的。所以咱们能够很容易就能理解到,一个函数中的this指向,能够是很是灵活的。

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。
若是调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。若是函数独立调用,那么该函数内部的this,则指向undefined。可是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

person.pro.getName()中,getName是调用者,他不是独立调用,被对象person.pro所拥有,所以它的this指向了person.pro。而pepole()做为调用者,尽管他与person.pro.getName的引用相同,可是它是独立调用的,所以this指向undefined,在非严格模式,自动转向全局window。

再来看一个例子,来加深理解这段话:

var a = 20;
function getA() {
    return this.a;
}
var foo = {
    a: 10,
    getA: getA
}
console.log(foo.getA());  // 10

灵机一动,再来一个。以下例子。

function foo() {
    console.log(this.a)
}

function active(fn) {
    fn(); // 真实调用者,为独立调用
}

var a = 20;
var obj = {
    a: 10,
    getA: foo
}
active(obj.getA);

这个例子提示一下,关于函数参数的传递赋值问题。
JS是按值传递仍是按引用传递?
这里我就很少作解答了,你们自行揣摩。

以上关于this解答来自波同窗的引用,我这里就偷了个懒在,直接拿来引用。
原文地址:前端基础进阶(五):全方位解读this

最后把知道面试题梳理一下:

console.log(person.pro.getName());//Michael
var pepole = person.pro.getName;
console.log(pepole());//jay

person.pro.getName()中,getName是调用者,他不是独立调用,被对象person.pro所拥有,所以它的this指向了person.pro,因此this.name=person.pro.name="Michael";

而pepole()做为调用者,尽管他与person.pro.getName的引用相同,可是它是独立调用的,所以this指向undefined,在非严格模式,自动转向全局window。
这道题实在非严格模式下,因此this指向了window,又由于全局变量挂载在window对象下,因此this.name=window.name=“jay”

完毕~写的有点啰嗦,只是尽可能想说明白,讲清一些概念的东西,反正我是收获不少,你呢?

参考文章:
JavaScript 全局对象
原生JS执行环境与做用域深刻理解
理解Javascript_12_执行模型浅析
前端基础进阶(二):执行上下文详细图解
前端基础进阶(五):全方位解读this

相关文章
相关标签/搜索