相信大家在入门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. 3.2 定位shellcode

    前言 此帖为 0day_2th 一书第三章实践不完全记录. 流程记录 searchAddr.c 文件: #include <windows.h> #include <stdio.h& ...

  2. com.netflix.zuul.exception.ZuulException: Forwarding error

    一.问题描述 在使用Spring Cloud的zuul组件,做路由转发时,每次重新启动后端服务,头几次调用都会出现com.netflix.zuul.exception.ZuulException: F ...

  3. LeetCode--004--寻找两个有序数组的中位数(java)

    转自https://blog.csdn.net/chen_xinjia/article/details/69258706 其中,N1=4,N2=6,size=4+6=10. 1,现在有的是两个已经排好 ...

  4. DPDK 16.04/16.11.2 默认tx offload是关闭的引起tx vlan offload无效

    打开IXGBE调试日志发发现:tx使用ixgbe_xmit_pkts_vec,默认tx offload无效了PMD: ixgbe_set_tx_function(): Using simple tx ...

  5. Remove Element leetcode java

    问题描述: Given an array and a value, remove all instances of that value in place and return the new len ...

  6. 漏洞复现——ngnix文件解析漏洞

    漏洞描述: 上传文件时,在文件名后加%00php,就可以绕过检测成功上传而已文件 影响版本: nginx 0.8.41 – 1.5.6 漏洞分析: 该漏洞原理是非法字符空格和截止符(\0)会导致Ngi ...

  7. 牛客练习赛32-D-MST+tarjin割边

    链接:https://ac.nowcoder.com/acm/contest/272/D来源:牛客网 题目描述 小p和他的朋友约定好去游乐场游玩,但是他们到了游乐场后却互相找不到对方了. 游乐场可以看 ...

  8. 46. Permutations C++回溯法

    基本的回溯法 注意每次回溯回来要把上次push_back()进去的数字pop掉! class Solution { public: void backTrack(vector<int> n ...

  9. 1.两数之和(Two Sum) C++

    暴力法可解决,速度很慢. 解决办法:哈希表 知识点: map的构造 遍历map使用迭代器,判断条件 插入 pair<int,int> 寻找key是否存在 class Solution { ...

  10. docker实战系列之docker 端口映射错误解决方法

    错误: Error response from daemon: Cannot start container web: iptables failed: iptables -t nat -A DOCK ...