JS被很多人认为是『拙劣的语言』,被这门语言里的各种离奇的事情整的团团转,这篇文章主要来讲讲JS中的Scope链,其主要是影响JS中的变量作用域。

注:本文适合稍有一定JS基础的同学

目录:


  1. 初步认识
  2. 预编译
  3. 不同Scope进行操作
  4. Scope链
  5. 例题
  6. this变量
  7. new操作符

初步认识

首先,来看一段代码:

var a = 1;

if(true){
var b = 1;
} console.log(b) //1

我们从最基本的开始,在面向对象的强语言中(Java,C……),其作用域都是基于块的(即:{}),块内可以对块外的变量进行操作,但是块外却对块内的变量是无法操作的。但是JS呢?一门弱语言,其并没有实现基于块的作用域,而是基于function的,因此上面的代码运行出来的结果b并不是undefined,说明最终a和b是定义在一个Scope内的。

其Scope如图:

预编译

var a = 1;
var b = 2; function doit(){
console.log(b);
var b = 3;
console.log(b);
} doit(); console.log(b); //undefined
//3
//2

讲到了Scope,不得不讲一讲js的预编译,为什么我们得到的第一个log的结果为undefined呢?按照强语言的思路来说这里应该是2才对呀,这就是js的预编译。js的代码在首次被加载完成后进行编译时,会将所有的function和var提前进行声明,但是并不会对其进行赋值,赋值则都是在该代码块进行执行时才会对其进行赋值,那么第一个log则是在预编译为b进行了声明后,这时b是没有时行赋值的,所以会log出undefined。

由于js是基于function来创建Scope的,所以只有doit执行时才会创建新的Scope,其Scope如图:

如果不对变量进行var的话,它是不会存在于function执行的时创建的新Scope的。

不同Scope进行操作

var a = 1;
var b = 2; function doit(){
var b = 3;
console.log(a);
} doit();

在doit的function里,加了一句console.log(a),这里就有个问题了,这里的a是从哪里来呢?

这里的doit funciton在执行的时候创建了一个新的Scope,如上一个例子讲的,但是这个Scope里只创建了b变量,去哪找a呢?我们把上面这个换一换,才是真正正确的Scope:

在js中,var和function在预编译中,作用都是一样的,都是提前声明了变量,但是并没有对其进行赋值,所以这段代码完整的Scope应该是拥有这三个变量的,只是doit是一个指向堆的引用。而在doit执行时,才会创建新的Scope,由于js语言的特殊性,虽然doit在这个Scope里定义的,但是其执行环境可以通过引用改变到任何地方,但是doit这个函数的定义环境永远都是确定的,即这个Scope内。

我们使用__proto__的思想去理解Scope链,当函数执行时,会在新的Scope内创建一个引用(我们假设它为__parent__),而这个引用指向则是在function定义时的Scope,在进行变量查找时,则会先在自身的Scope内进行查找,如果没有找到变量,则会根据__parent__来查找到定义时的Scope,在该Scope里进行变量的查找,如图:

Scope链

就像上图那样,每个Scope都拥有一个__parent__,所有即使这个function无论在什么环境中进行执行,其父Scope都是这个function创建的Scope,虽然js很乱,但是是乱得有规有矩。

当代码在浏览器中执行,去查找变量时,往往都是如下图过程:

优先查找自己Scope,如果查找不到则根据Scope链去查找最近的同名变量,如果一直查找到了Top Scope(在浏览器中则是window)还未找到的话,则这个变量会被认为_"Uncaught ReferenceError: **** is not defined"_,如果直接使用的话则会报错。

我们为什么可以在代码中的任何地方使用document,location等太多变量,都是一直通过Scope链查找到了Top Scope从window中取得的。

例题

拿来最基础的前端面试题来进行分析:

<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button> <script>
var buttons = document.getElementsByTagName("button"); for(var i = 0, l = buttons.length; i < l; i++){
buttons[i].onclick = function(){
alert(i);
}
}
</script>

很多人知道这个例子最终结果是什么样的,即点击每个button则都alert(6),并没有达到我们预想中的结果,但是大部分人并不能对这个问题说出个所以然,只能说到“最后不就是加到6了嘛”。我们从Scope链来分析这个例子,这里遍历了6次,定义了6个匿名function,并将其赋值于了不同按钮的onclick事件,而这6个匿名function的定义Scope都是相同的,当用户进行点击时,会执行对应的一个匿名function,该function创建的Scope中并没有i这个变量,所以它会根据__parent__来找到定义这个function的Scope,找到了i,但是这个i的这时值为6,并且这六个function都是找到了这个值为6的i,所以点击它们都会相同地弹出i这个值。

正确的写法有很多,但是思路只有一个,那就是改变匿名函数的创建Scope,并且该Scope又与i存在的Scope不同,这就是大家说的闭包,其实闭包就是Scope,每个函数都会创建Scope,创建闭包,下面两种写法都是改变了匿名函数的创建Scope,并在该Scope中保存了独一无二的index值。

写法1:

for(var i = 0, l = buttons.length; i < l; i++){
(function(index)
buttons[index].onclick = function(){
alert(index);
}
)(i);
}

写法2:

for(var i = 0, l = buttons.length; i < l; i++){
buttons[i].onclick = (function(index){
return function(){
alert(index);
}
})(i);
}

this变量

this作为js中最为灵活的变量,也是弄晕了一批一批青年们。开始之前,我们先来上一段代码:

var a = 1;
function doit(){
var b = 2; return function(){
var c = 3;
console.log(this);
}
} doit()();

那么问题来了,这里的this会和a,b,c中的哪一个变量有关系呢?

先思考一段时间

···

···

···

···

答案是a,通过this.a可以获取到a的值,即:1。

对于this,我们可以理解为:特殊的Scope引用变量,其指向当前函数的执行环境Scope(并不是定义时的Scope)


我们用上面的例子来理解,虽然this是写在最里面的function的,但是这个function的最终执行是在最外面的Scope进行执行的,所以this指向的是最外层的Scope,而a是定义在最外层的Scope中的,则这时我们可以使用this.a来获取到a的值。

在使用this时:明确了this指向的Scope再使用this,由于js的对引用并没有限制,所以这个函数的执行环境永远是不确定的,所以this去对应的Scope中取值时是不一定能取得到的。

callapply则可以去改变函数执行的Scope,从而改变this的指向,对于这两个方法的使用,这里不再详解。

new操作符

js作为一个弱语言,在ES6之前并没有class之说,现在所有浏览器都不直接支持ES6(除非手动打开),但是我们想要实现对类的创建该怎么做呢?乒乒乓乓来段代码:

function Person(name,sex,age){
this.name = name;
this.sex = sex; var Age = age;
} var man = new Person("Bob", "male", "17");

在这个new的过程中,它到底做了什么呢?我们来按步分析一下:

1.创建Object Scope

创建一个空的Scope

2.在该Object Scope下执行

Person("Bob", "male", "17");

则这时Person里的this是指向这个Object Scope,所以this.namethis.sex则是为Object Scope赋值了新的变量和值。

3.得到:

而Age去哪了呢?根据上面Scope的知道,Age则是被创建在Person自身的Scope内,并非Object Scope,这时Person函数创建出来的Scope则拥有四个变量,即:name,sex,age,Age;这个Age就像是强语言中的private一样,外界是无法获取到的,这样我们则会生成一个类似于类的实现方法。

所以来说,下面的代码:

console.log(man.name) //Bob
console.log(man.sex) //male
console.log(man.Age) //undefined

我们可以得到name和sex,但是并不能得到Age。

End

对于Scope的介绍结束啦,希望本文能为你更深地理解js起到帮助,BTW,js并不是拙劣的语言,当你真正熟悉了它,你会觉得它如此地好用

Finish.

深入理解JS之Scope链的更多相关文章

  1. 怎么理解js的原型链继承?

    前言 了解java等面向对象语言的童鞋应该知道.面向对象的三大特性就是:封装,继承,多态. 今天,我们就来聊一聊继承.但是,注意,我们现在说的是js的继承. 在js的es6语法出来之前,我们想实现js ...

  2. 一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends

    说实在话,以前我只需要知道"寄生组合继承"是最好的,有个祖传代码模版用就行.最近因为一些事情,几个星期以来一直心心念念想整理出来.本文以<JavaScript高级程序设计&g ...

  3. 深入理解JS函数作用域链与闭包问题

    function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } ); a.fun(); a.f ...

  4. 理解js中的作用域,作用域链以及闭包

    作用域变量作用域的类型:全局变量和局部变量全局作用域对于最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的 <script> var outerVar = " ...

  5. js基础梳理-如何理解作用域和作用域链?

    本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识. 1.什么是作用域(scope)? 在<JavaScritp高级程序设计>中并没有找到确切的关 ...

  6. 前端基本知识(二):JS的原始链的理解

    之前一直对于前端的基本知识不是了解很详细,基本功不扎实,但是前端开发中的基本知识才是以后职业发展的根基,虽然自己总是以一种实践是检验真理的唯一标准,写代码实践项目才是唯一,但是经常遇到知道怎么去解决这 ...

  7. 前端基本知识(二):JS的原型链的理解

    之前一直对于前端的基本知识不是了解很详细,基本功不扎实,但是前端开发中的基本知识才是以后职业发展的根基,虽然自己总是以一种实践是检验真理的唯一标准,写代码实践项目才是唯一,但是经常遇到知道怎么去解决这 ...

  8. 简单粗暴地理解js原型链–js面向对象编程

    简单粗暴地理解js原型链–js面向对象编程 作者:茄果 链接:http://www.cnblogs.com/qieguo/archive/2016/05/03/5451626.html 原型链理解起来 ...

  9. 【学习笔记】深入理解js原型和闭包(14)——从【自由变量】到【作用域链】

    先解释一下什么是“自由变量”. 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量.如下图 如上程序中,在调用fn()函数时,函数体中第6 ...

随机推荐

  1. 14条最佳JS代码编写技巧

    http://gaohaixian.blog.163.com/blog/static/123260105201142645458315/写任何编程代码,不同的开发者都会有不同的见解.但参考一下总是好的 ...

  2. cat /proc/iomem

    在proc目录下有iomem和ioports文件,其主要描述了系统的io内存和io端口资源分布. 对于外设的访问,最终都是通过读写设备上的寄存器实现的,寄存器不外乎:控制寄存器.状态寄存器和数据寄存器 ...

  3. Android EditText 赋值与取值

    //取值 String strSmsPhone=m_txtSmsPhone.getText().toString(); //赋值 m_txtSmsPhone.setText("你好" ...

  4. 使用jquery的 uploadify,在谷歌浏览器上总会崩溃的解决方法

    最近做的项目使用了jquery的uploadify,但是在谷歌浏览器测试总是会出现崩溃.如: 因为是java项目. 解决的办法是: 给引入的js加上一个参数,时间戳就可以,防止缓存,使每一次都请求.( ...

  5. Zepto源代码分析一~核心方法

    今天抽出时间复习了一下Zepto的源代码,依照自己的理解进行凝视. 欢迎大家拍砖. 源代码版本号:v1.1.4 源代码下载地址:http://zeptojs.com/ 分析总体代码之后,整理出架构图: ...

  6. Web服务(Web Service)相关概念

    1.概述 Web服务技术(Web Service )是一种面向服务的架构技术,通过标准的Web协议提供服务,保证不同平台的应用服务能够互相操作. 因为Web服务公布的数据基于XML格式和 SOAP协议 ...

  7. NAT Network Address Translation,网络地址转换

    Network Address Translation,网络地址转换

  8. 如何查看Linux操作系统的位数

    如何查看Linux操作系统的位数 1.编程实现: 在程序中返回sizeof(int)的值,返回的结果是操作系统的字节数.若返回4则是32位操作系统,返回8即是64位. 2.2.getconf命令: g ...

  9. XCode5中新建工程后强制使用了ARC,如何去掉?

    打开你的工程,点击目录的工程文件,最顶端蓝色的,然后选择project下你的工程,还是蓝色那项,然后build Settings,然后往下拉,在Apple LLVM 5.0 - Language - ...

  10. samba 文件和目录权限控制

    [laps_test]         comment = laps_test         path = /home/laps         browseable = yes         w ...