深入理解JS之Scope链
2015-09-28 23:21
513 查看
JS被很多人认为是『拙劣的语言』,被这门语言里的各种离奇的事情整的团团转,这篇文章主要来讲讲JS中的Scope链,其主要是影响JS中的变量作用域。
注:本文适合稍有一定JS基础的同学
预编译
不同Scope进行操作
Scope链
例题
this变量
new操作符
我们从最基本的开始,在面向对象的强语言中(Java,C……),其作用域都是基于块的(即:{}),块内可以对块外的变量进行操作,但是块外却对块内的变量是无法操作的。但是JS呢?一门弱语言,其并没有实现基于块的作用域,而是基于
其Scope如图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/b4f73d1262ae3f588baa7ee63bb0a37d.png)
讲到了Scope,不得不讲一讲js的预编译,为什么我们得到的第一个log的结果为undefined呢?按照强语言的思路来说这里应该是2才对呀,这就是js的预编译。js的代码在首次被加载完成后进行编译时,会将所有的function和var提前进行声明,但是并不会对其进行赋值,赋值则都是在该代码块进行执行时才会对其进行赋值,那么第一个log则是在
由于js是基于function来创建Scope的,所以只有doit执行时才会创建新的Scope,其Scope如图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/5a45b526679c14099e7e98a86cc557fb.png)
如果不对变量进行var的话,它是不会存在于function执行的时创建的新Scope的。
在doit的function里,加了一句
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/b4d5b252fc3e3862b9c5e681465bac6e.png)
这里的doit funciton在执行的时候创建了一个新的Scope,如上一个例子讲的,但是这个Scope里只创建了b变量,去哪找a呢?我们把上面这个换一换,才是真正正确的Scope:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/1953eded0ed6f832b6e4f007fcecfdbd.png)
在js中,var和function在预编译中,作用都是一样的,都是提前声明了变量,但是并没有对其进行赋值,所以这段代码完整的Scope应该是拥有这三个变量的,只是doit是一个指向堆的引用。而在doit执行时,才会创建新的Scope,由于js语言的特殊性,虽然doit在这个Scope里定义的,但是其执行环境可以通过引用改变到任何地方,但是doit这个函数的定义环境永远都是确定的,即这个Scope内。
我们使用
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/76c71b6a4eca9620e434694207b6ebe1.png)
当代码在浏览器中执行,去查找变量时,往往都是如下图过程:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/205073beeb22d832584e014ab24e260e.png)
优先查找自己Scope,如果查找不到则根据Scope链去查找最近的同名变量,如果一直查找到了Top Scope(在浏览器中则是window)还未找到的话,则这个变量会被认为_"Uncaught ReferenceError: **** is not defined"_,如果直接使用的话则会报错。
我们为什么可以在代码中的任何地方使用
很多人知道这个例子最终结果是什么样的,即点击每个button则都alert(6),并没有达到我们预想中的结果,但是大部分人并不能对这个问题说出个所以然,只能说到“最后不就是加到6了嘛”。我们从Scope链来分析这个例子,这里遍历了6次,定义了6个匿名function,并将其赋值于了不同按钮的onclick事件,而这6个匿名function的定义Scope都是相同的,当用户进行点击时,会执行对应的一个匿名function,该function创建的Scope中并没有i这个变量,所以它会根据
正确的写法有很多,但是思路只有一个,那就是改变匿名函数的创建Scope,并且该Scope又与i存在的Scope不同,这就是大家说的闭包,其实闭包就是Scope,每个函数都会创建Scope,创建闭包,下面两种写法都是改变了匿名函数的创建Scope,并在该Scope中保存了独一无二的index值。
写法1:
写法2:
那么问题来了,这里的
先思考一段时间
···
···
···
···
答案是
对于
我们用上面的例子来理解,虽然
在使用this时:明确了this指向的Scope再使用this,由于js的对引用并没有限制,所以这个函数的执行环境永远是不确定的,所以this去对应的Scope中取值时是不一定能取得到的。
在这个
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/a6bbdc81f8b362830f642c776852fc9e.png)
创建一个空的Scope
则这时Person里的this是指向这个Object Scope,所以
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/d6d8d8702418f5907b106a32d7eddfa3.png)
而Age去哪了呢?根据上面Scope的知道,Age则是被创建在Person自身的Scope内,并非Object Scope,这时Person函数创建出来的Scope则拥有四个变量,即:
所以来说,下面的代码:
我们可以得到name和sex,但是并不能得到Age。
Finish.
注:本文适合稍有一定JS基础的同学
目录:
初步认识预编译
不同Scope进行操作
Scope链
例题
this变量
new操作符
初步认识
首先,来看一段代码:var a = 1; if(true){ var b = 1; } console.log(b) //1
我们从最基本的开始,在面向对象的强语言中(Java,C……),其作用域都是基于块的(即:{}),块内可以对块外的变量进行操作,但是块外却对块内的变量是无法操作的。但是JS呢?一门弱语言,其并没有实现基于块的作用域,而是基于
function的,因此上面的代码运行出来的结果b并不是undefined,说明最终a和b是定义在一个Scope内的。
其Scope如图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/b4f73d1262ae3f588baa7ee63bb0a37d.png)
预编译
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如图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/5a45b526679c14099e7e98a86cc557fb.png)
如果不对变量进行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是从哪里来呢?
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/b4d5b252fc3e3862b9c5e681465bac6e.png)
这里的doit funciton在执行的时候创建了一个新的Scope,如上一个例子讲的,但是这个Scope里只创建了b变量,去哪找a呢?我们把上面这个换一换,才是真正正确的Scope:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/1953eded0ed6f832b6e4f007fcecfdbd.png)
在js中,var和function在预编译中,作用都是一样的,都是提前声明了变量,但是并没有对其进行赋值,所以这段代码完整的Scope应该是拥有这三个变量的,只是doit是一个指向堆的引用。而在doit执行时,才会创建新的Scope,由于js语言的特殊性,虽然doit在这个Scope里定义的,但是其执行环境可以通过引用改变到任何地方,但是doit这个函数的定义环境永远都是确定的,即这个Scope内。
我们使用
__proto__的思想去理解Scope链,当函数执行时,会在新的Scope内创建一个引用(我们假设它为
__parent__),而这个引用指向则是在function定义时的Scope,在进行变量查找时,则会先在自身的Scope内进行查找,如果没有找到变量,则会根据
__parent__来查找到定义时的Scope,在该Scope里进行变量的查找,如图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/76c71b6a4eca9620e434694207b6ebe1.png)
Scope链
就像上图那样,每个Scope都拥有一个__parent__,所有即使这个function无论在什么环境中进行执行,其父Scope都是这个function创建的Scope,虽然js很乱,但是是乱得有规有矩。
当代码在浏览器中执行,去查找变量时,往往都是如下图过程:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/205073beeb22d832584e014ab24e260e.png)
优先查找自己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中取值时是不一定能取得到的。
call和
apply则可以去改变函数执行的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
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/a6bbdc81f8b362830f642c776852fc9e.png)
创建一个空的Scope
2.在该Object Scope下执行
Person("Bob", "male", "17");
则这时Person里的this是指向这个Object Scope,所以
this.name与
this.sex则是为Object Scope赋值了新的变量和值。
3.得到:
![](https://oscdn.geek-share.com/Uploads/Images/Content/201509/d6d8d8702418f5907b106a32d7eddfa3.png)
而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.
相关文章推荐
- JavaScript高级程序设计之数据类型
- Servlet&JSP思维导图
- Js 图片上传本地预览
- JavaScript 概述
- jsoup
- javascript运算符之相等与严格相等
- 在Javascript定义对象
- js判断是否为空
- 《Javascript权威指南》13号学习笔记:使用日期和时间
- JS之变量的运算
- JS校验表单项
- JavaScript匿名函数,动态函数,可变参数函数
- JS浮点运算错误的解决
- 深入理解js闭包
- 详解js闭包
- JavaScript总结3—对象
- 【JSOI 2008】【BZOJ 1017】魔兽地图DotR
- JavaScript--面向对象基础
- JS对select及option的操作详解
- Javascript自定义事件