相信大家在入门JavaScript这门语言时对作用域、作用域链、变量声明提升这些概念肯定会稀里糊涂,下面就来说说这几个

Javascript 作用域

在 Javascript 中,只有局部作用域和全局作用域。而只有函数可以创建局部作用域,像 if,for 或者 while 这种块语句是没办法创建作用域的。 (当然 ES6 提供了 let 关键字可以创建块作用域。)

Javascript 的这种特性导致 for 循环里面创建闭包时会产生让人意想不到的结果。比如下面这个例子:

var i = 20;
var makeLogger = function() {
var funcs = [];
for(var i = 0; i < 10; ++i){
funcs[i] = function() {
console.log(i);
}
}
return funcs;
} var loggers = makeLogger();
for(var i = 0; i < 10; ++i){
loggers[i]();
}

RESULTS:

上面的输出结果,大致原因就是 for 循环里面的变量的作用域是整个函数的,循环内部创建的一系列闭包引用的是同一个变量 i,而在 for 循环结束后,这个 i 的值变成了 10。所以当我们调用这些内部函数的时候,就会输出 10 了。

现在这样讲可能还是不够清楚,在我们了解作用域链和 Javascript 的执行原理后,就更容易理解了。

Javascript 作用域链

  1. 当 Js 里面 声明 一个函数的时候,会给该函数对象创建一个 scope 属性,该属性指向当前作用域链对象。
  2. 当 Js 里面 调用 一个函数的时候,会创建一个执行上下文,这个执行上下文定义了函数解释执行时的环境信息。每个执行上下文都有自己的作用域链,主要用于变量标识符的解析。
  3. 在 Js 引擎运行一个函数的时候,它首先会把该函数的 scope 属性添加到执行上下文的作用域链上面,然后再创建一个 活动对象 添加到此作用域顶端共同组成了新的作用域链。活动对象包含了该函数的所有的形参,arguments 对象,所有的局变变量等信息。
  4. 当解释执行函数的每一条语句的时,会依据这个执行上下文的作用域链来查找标识符,如果在一个作用域对象上面没有找到标识符,则会沿着作用链一直向上查找,这一点类似于 Js 的原型继承的属性查找机制。

让我们来看几个具体的例子:

var name = 'zilongshanren';
function echo() {
console.log(name);
var name = 'hello';
console.log(name);
} echo();

RESULTS:

undefined
hello

要理解上面的代码的输出结果,我们可以按照上面提到的 4 点来解释:

  • 在声明 echo 函数时,此时的作用域链是(我们假设 scope chain 是一个作用域对象数组)
[[scope chain]] = [
{
global Object: {
name: 'zilongshanren'
...
}
}
]

echo 函数的作用域属性指向此 scope chain 对象。

  • 当调用 echo 函数时,会创建一个执行上下文,同时把 echo 的作用域添加到执行上下文的作用域链上。同时创建一个活动对象并添加到该作用域链的顶端。此时的作用域链是:
[[scope chain]] = [
{
Active Object {
name: undefined,
arguments: ...
...
},
global Object: {
name: 'zilongshanren'
...
}
}
]
  • 当解释执行函数的第一条语句的时候,查找 name 变量,在活动对象中找到了,于是输出 undefined。然后执行 var name = 'hello',此时变量 name 的值为 hello。最后解释执行 console.log(name)的时候就输出了 hello.

这个例子可能比较简单,因为它没有使用闭包。

我们接下来分解一下本文开头的例子。

  • 当定义 makeLogger 函数时,makeLogger 函数的作用域为:
[[scope chain]] = [
{
global Object: {
i: 20,
...
}
}
]
  • 在 for 循环里面定义闭包函数的时候,此时的作用域链是:
[[scope chain]] = [
{
makeLogger local scope object : {
i: undefined,
funcs: [],
},
global Object: {
i: 20,
...
}
}
]

并且此时 funcs…funcs的 scope 都指向该 scope chain。

  • 当调用 makeLogger 函数的时候,创建一个执行上下文。把 makeLogger 函数的作用域链加到执行上下文中,并且创建一个活动对象添加到作用域链的顶端,此时的 scope chain 为:
[[scope chain]] = [
{
makeLogger active object: {
funcs: undefined,
i: undefined,
arguments: ...
},
global Object: {
i: 20,
...
}
}
]
  • 当执行完 makeLogger 函数的时候,此时的作用域对象变成了:
[[scope chain]] = [
{
makeLogger local scope object : {
i: undefined,
funcs: [function object ...],
},
global Object: {
i: 20,
...
}
}
]

这里的 funcs 函数还会生成闭包对象,它包含了 makeLogger 局部作用域的变量的值,即 i=10.

下图是 V8 引擎中 funcs 函数及其闭包的截图:

  • 最后遍历执行所有的 loggers 的时候,会依次为每一个 loggers 函数创建一个执行上下文,每一个执行上下文的作用域链为:
[[scope chain]] = [
{
loggers function active object : {
arguments: ...
},
makeLogger local scope object : {
i: 10,
funcs: [function object ...],
},
global Object: {
i: 20,
...
}
}
]

当执行 loggers 函数的 console.log(i)的时候,它会沿着此时的作用域链进行变量查找,于是找到了 i=10. 所以我们输出的结果就是 10.

变量提升

我们看一个例子:

var name = 'zilongshanren';
function echo() {
name = "hello";
console.log(name);
var name;
console.log(name);
}
console.log(name); echo();

+RESULTS:

zilongshanren
hello
hello
undefined

调用 echo 函数的第一行 name = "hello"时并不是对全局变量 name 进行重新赋值,而是对函数内部声明的变量 name 进行赋值。所以,在 echo 函数声明之后,调用 console.log(name)输出的还是 zilongshanren。

echo 函数内部的 name 变量“使用在前,而声明在后”,这就是所谓的变量提升。

如果从我们前面提到的变量作用域和作用域链来解释这个行为肯定是更容易理解的。

正因为函数内部的变量声明会发生“提升”副作用,所以,最好的做法就是把函数需要用到的局部变量都放在函数开头进行声明,避免产生不必要的混淆。

小结

JavaScript 中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。理解作用域和作用域链对于理解闭包和变量提升这种奇葩特性非常有帮助。 本文可能有些地方讲的还不是非常清楚,读者可以读一读后面的参考链接,相信会有助于理解。

JavaScript作用域及作用域链详解、声明提升的更多相关文章

  1. JS作用域,作用域,作用链详解

    前言   通过本文,你大概明白作用域,作用域链是什么,毕竟这也算JS中的基本概念. 一.作用域(scope) 什么是作用域,你可以理解为你所声明变量的可用范围,我在某个范围内申明了一个变量,且这个变量 ...

  2. 《前端之路》之 JavaScript原型及原型链详解

    05:JS 原型链 在 JavaScript 的世界中,万物皆对象! 但是这各种各样的对象其实具体来划分的话就 2 种. 一种是 函数对象,剩下的就是 普通对象.其中 Function 和 Objec ...

  3. javascript 原型及原型链详解

    我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个原型对象,而这个原型对象中拥有的属性和方法可以被所以实例共享. function Person(){ } Pe ...

  4. 你不知道的JavaScript--Item15 prototype原型和原型链详解

    用过JavaScript的同学们肯定都对prototype如雷贯耳,但是这究竟是个什么东西却让初学者莫衷一是,只知道函数都会有一个prototype属性,可以为其添加函数供实例访问,其它的就不清楚了, ...

  5. “全栈2019”Java异常第十五章:异常链详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

  6. 高性能JavaScript模板引擎实现原理详解

    这篇文章主要介绍了JavaScript模板引擎实现原理详解,本文着重讲解artTemplate模板的实现原理,它采用预编译方式让性能有了质的飞跃,是其它知名模板引擎的25.32 倍,需要的朋友可以参考 ...

  7. JavaScript对象的property属性详解

    JavaScript对象的property属性详解:https://www.jb51.net/article/48594.htm JS原型与原型链终极详解_proto_.prototype及const ...

  8. javascript中=、==、===区别详解

    javascript中=.==.===区别详解今天在项目开发过中发现在一个小问题.在判断n==""结果当n=0时 n==""结果也返回了true.虽然是个小问题 ...

  9. Javascript 调试利器 Firebug使用详解

    Javascript 调试利器 Firebug使用详解 有时候,为了更清楚方便的查看输出信息,我们可能需要将一些调试信息进行分组输出,那么可以使用console.group来对信息进行分组,在组信息输 ...

随机推荐

  1. 2019/01/17 基于windows使用fabric将gitlab的文件远程同步到服务器(git)

    觉得django项目把本地更新push到gitlab,再执行fabric脚本从gitlab更新服务器项目挺方便的,当然从本地直接到服务器就比较灵活. 2019/01/17 基于windows使用fab ...

  2. 跨域和jsonp的原理

    什么是跨域? 跨域,指的是浏览器不能执行其他网站的脚本.它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制. 所谓同源是指,域名,协议,端口均相同,不明白没关系,举个栗子: h ...

  3. Shiro中Realm

    6.1 Realm [2.5 Realm]及[3.5 Authorizer]部分都已经详细介绍过Realm了,接下来再来看一下一般真实环境下的Realm如何实现. 1.定义实体及关系   即用户-角色 ...

  4. Beta阶段——第1篇 Scrum 冲刺博客

    第1篇 Scrum 冲刺博客 a. 介绍小组新加入的成员,Ta担任的角色. 新加入成员 郭炜埕 原先担任的角色 前端界面设计 现在担任的角色 前端开发,并协助后端开发 新加成员介绍 炜埕同学对界面设计 ...

  5. ORACLE PACKAGE中查看包的依赖关系

    SELECT dd.* FROM dba_dependencies dd WHERE NAME <> referenced_name AND referenced_type <> ...

  6. Python序列化-pickle和json模块

    Python的“file-like object“就是一种鸭子类型.对真正的文件对象,它有一个read()方法,返回其内容.但是,许多对象,只要有read()方法,都被视为“file-like obj ...

  7. 一、集合框架(关于ArrayList,LinkedList,HashSet,LinkedHashSet,TreeSet)

    一.ArrayList 解决了数组的局限性,最常见的容器类,ArrayList容器的容量capacity会随着对象的增加,自动增长.不会出现数组边界的问题. package collection;   ...

  8. Hadoop---静动态增删节点

    静动态增删节点 初始: 这是我现在的集群 重新克隆一个虚拟机: hadoop44:datanode(静态增加,删除和动态删除) hadoop55:datanode(动态hdfs添加,yarn动态增删) ...

  9. dynamic load jar and init spring

    public class SpringLoader { private Map<String, Class<?>> classMap = new HashMap<> ...

  10. springMVC中对HTTP请求form data和request payload两种数据发送块的后台接收方式

    最近在做项目中发现,前台提交数据时,如果通过form表单提交和ajax发送json时,springMVC后台接收不能都通过@ModelAttribute方式处理,经过一番查找后,ajax发送json请 ...