来源:https://www.cnblogs.com/hezhi/archive/2018/12/09/10090151.html
本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识。
在《JavaScritp高级程序设计》中并没有找到确切的关于作用域的定义,只是在“4.2执行环境及作用域”中简单说了下执行环境(execution context)的概念。
而在《JavaScript权威指南》中,对作用域的描述为:
变量作用域:一个变量的作用域(scope)是程序源代码中定义这个变量的区域
在《你不知道的JavaScript·上卷》中对作用域的描述则为:
负责收集并维护由所有生命的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
简单来讲,作用域(scope)就是变量访问规则的有效范围。
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
说得很深奥的样子,其实上面这段话重点用函数作用域与函数执行上下文来区分是最好不过的了。函数作用域是在函数声明的时候就已经确定了,而函数执行上下文是在函数调用时创建的。假如一个函数被调用多次,那么它就会创建多个函数执行上下文,但是函数作用域显然不会跟着函数被调用的次数而发生什么变化。
var foo = 'foo';
console.log(window.foo); // => 'foo'
在浏览器环境中声明变量,该变量会默认成为window对象下的属性。
function foo() {
name = "bar"
}
foo();
console.log(window.name) // bar
在函数中,如果不加 var 声明一个变量,那么这个变量会默认被声明为全局变量,如果是严格模式,则会报错。
全局变量会造成命名污染,如果在多处对同一个全局变量进行操作,那么久会覆盖全局变量的定义。同时全局变量数量过多,非常不方便管理。
这也是为什么jquery要在全局建立下的原因。
假如在函数中定义一个局部变量,那么该变量只可以在该函数作用域中被访问。
function doSomething () {
var thing = '吃早餐';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined
嵌套函数作用域:
function outer () {
var thing = '吃早餐';
function inner () {
console.log(thing);
}
inner();
}
outer(); // 吃早餐
在外层函数中,嵌套一个内层函数,那么这个内层函数可以向上访问到外层函数中的变量。
既然内层函数可以访问到外层函数的变量,那如果把内层函数return出来会怎样?
function outer () {
var thing = '吃早餐';
function inner () {
console.log(thing);
}
return inner;
}
var foo = outer();
foo(); // 吃早餐
前面提到,函数执行完后,函数作用域的变量就会被垃圾回收。而这段代码看出当返回了一个访问了外部函数变量的内部函数,最后外部函数的变量得以保存。
这种当变量存在的函数已经执行结束,但扔可以再次被访问到的方式就是“闭包”。后期会继续对闭包进行梳理。
很多书上都有一句话,javascript没有块级作用域的概念。所谓块级作用域,就是{}包裹的区域。但是在ES6出来以后,这句话并不那么正确了。因为可以用 let 或者 const 声明一个块级作用域的变量或常量。
比如:
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // Uncaught ReferenceError: i is not defined
发现这个例子就会和函数作用域中的第一个例子一样的错误提示。因为变量i只可以在 for循环的{ }块级作用域中被访问了。
扩散思考:
究竟什么时候该用let?什么时候该用const?
默认使用 const,只有当确实需要改变变量的值的时候才使用let。因为大部分的变量的值在初始化之后不应再改变,而预料之外的变量的修改是很多bug的源头。
词法作用域,也可以叫做静态作用域。意思是无论函数在哪里调用,词法作用域都只在由函数被声明时所处的位置决定。
既然有静态作用域,那么也有动态作用域。
而动态作用域的作用域则是由函数被调用时执行的位置所决定。
var a = 123;
function fn1 () {
console.log(a);
}
function fn2 () {
var a = 456;
fn1();
}
fn2(); // 123
以上代码,最后输出结果 a 的值,来自于 fn1 声明时所在位置访问到的 a 值 123。
所以js的作用域是静态作用域,也叫词法作用域。
上面的1.1-1.3可以看做作用域的类型。而这一小节,其实跟上面三小节还是有差别的,并不属于作用域的类型,只是关于作用域的一个补充说明吧。
在js引擎中,通过标识符查找标识符的值,会从当前作用域向上查找,直到作用域找到第一个匹配的标识符位置。就是JS的作用域链。
var a = 1;
function fn1 () {
var a = 2;
function fn2 () {
var a = 3;
console.log(a);
}
fn2 ();
}
fn1(); // 3
console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。
下面以一个函数的创建和激活两个时期来讲解作用域链是如何创建及变化的。
上文中讲到,函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,但是注意:此时[[scope]]并不代表完整的作用域链,因为在创建阶段,它还没有包括自己的作用域。
举个栗子:
function foo () {
function bar () {
...
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.AO
];
当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。
这时候执行上下文的作用域链,命名为 Scope:
Scope = [AO].concat([[scope]]);
至此,作用域链创建完毕。
以下面的例子为例,结合之前的变量对象,活动对象和执行上下文栈,总结一下函数执行上下文中作用域链和变量对象的创建过程:
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z);
}
bar();
}
foo(); // 60
大家肯定都知道打印结果会是60。但是从第一行代码开始到最后一行代码结束,整个代码的执行上下文栈以及作用域链是怎样变化的呢?
// 第一步:进入全局上下文,此时的执行上下文栈是这样:
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
];
// 第二步:foo函数被创建,此时的执行上下文栈没有变化,但是创建了foo函数的作用域,保存作用域链到内部属性[[scope]]。
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
];
foo.[[scope]] = [
globalContext.VO
];
// 第三步:foo函数执行,进入foo函数上下文的创建阶段
// 这个阶段它做了三件事:
// 1.复制之前的foo.[[scope]]属性到foo函数上下文下,创建foo函数的作用域链;
// 2. 创建foo函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明
// 3. 把foo函数上下文的变量对象加入到第一步创建的foo函数作用域链的最前面。
// 最终,经过这三个步骤之后,整个执行上下文栈是这样
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
VO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: undefined
},
Scope: [foo.VO, globalContext.VO]
}
];
foo.[[scope]] = [
foo.VO,
globalContext.VO
];
// 第四步:foo函数执行,进入foo函数上下文的执行阶段。
// 这个阶段又做了以下2件事:
// 1. 把foo执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值
// 2. 发现创建了一个 bar函数,就保存了bar函数的所有父变量对象到bar函数的[[scope]]属性上。
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
}
];
foo.[[scope]] = [
foo.AO,
globalContext.VO
];
bar.[[scope]] = [
foo.AO,
globalContext.VO
];
// 第五步,bar函数执行,进入bar函数上下文的创建阶段
// 与第三步类似,也做了三件事,只不过主体变成了bar
// 1.复制之前的bar.[[scope]]属性到bar函数上下文下,创建foo函数的作用域链;
// 2. 创建bar函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明
// 3. 把bar函数上下文的变量对象加入到第一步创建的bar函数作用域链的最前面。
// 最终,经过这三个步骤之后,整个执行上下文栈是这样
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
},
<bar>functionContext: {
VO: {
arguments: {
length: 0
},
z: undefined
},
Scope: [bar.VO, foo.AO, globalContext.VO]
}
];
foo.[[scope]] = [
foo.AO,
globalContext.VO
];
bar.[[scope]] = [
bar.VO,
foo.AO,
globalContext.VO
];
// 第六步:bar函数执行,进入bar函数上下文的执行阶段
// 与第四步类似。不过此时bar函数里面不会再创建新的函数上下文了
// 1. 把bar执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
},
<bar>functionContext: {
AO: {
arguments: {
length: 0
},
z: 30
},
Scope: [bar.AO, foo.AO, globalContext.VO]
}
];
foo.[[scope]] = [
foo.AO,
globalContext.VO
];
bar.[[scope]] = [
bar.AO,
foo.AO,
globalContext.VO
];
// 第七步:执行bar函数中的console.log(x + y +z),查找x,y,z三个标识符
- "x"
-- <bar>functionContext.AO // 没找到,继续到foo.AO中找
-- <foo>functionContext.AO // 还没找到,再往globalContext.VO中找
-- globalContext.VO // 找到了,值为 10
- "y"
-- <bar>functionContext.AO // 没找到,继续到foo.AO中找
-- <foo>functionContext.AO // 找到了,值为20
-- "z"
-- <bar>functionContext.AO // 找到了,值为 30
打印结果: 60。
// 第八步:bar函数执行完毕,将其从执行上下文栈中弹出,foo函数执行完毕,将其从执行上下文栈中弹出。最终,执行上下文栈,只剩下globalContext
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
]
感觉其实可以简化理解一下,把第三四步,第五六步分别分成一个步骤。
javascript实现数字三位逗号分隔,如把123456.78转换为123,456.78。js实现支持货币格式表示法:toLocaleString在将数字转换为字符串的同时,会使用三位分节法进行显示。slice 方法用于截取字符串中的一部分并返回该部分字符串。match方式代表正则表达式的匹配....
最近阅读《JavaScript忍者秘籍》看到了一种有趣的函数:自记忆函数。记忆化(memoization)是一种构建函数的处理过程,能够记住上次计算结果,当函数计算得到结果时,就将该结果按照参数存储起来。
一些常用的JavaScript 知识点整理,包括:两个函数是否等价、NaN是什么?它是什么类型?如何检测一个变量是否是NaN?作用域相关问题?js小数计算不准确的bug,js算法/思路相关,js类型强制转换
Javascript模仿接口可以有三种方式:1.注释法,此方法属于程序文档范畴,对接口的继承实现完全依靠程序员自觉 2.检查属性法,把要实现的接口方法添加到类属性列表里,通过定义好的检测反复检查是否已经实现了那些方法 3.鸭式辨形法
接口返回的是int类型的秒数,在前端显示要求拼接为时分秒显示,这篇文章主要讲解实现js秒数转换成时分秒的方法。
原来是想使用 cookie 来记录,但是考虑到 cookie 所能记录的数据最大为 4k ,可能不够用。于是使用了 HTML5 的 localStorage (最大数据 5M )来存储( IE8 以上浏览器支持)。这里使用到了 jquery.cookie 的插件,所以页面要引入 jquery 和 jquery.cookie 。
冒泡排序;插入排序 过程就像你拿到一副扑克牌然后对它排序一样;快速排序;回文字符串;翻转字符串;字符串中出现最多次数的字符;数组去重;二分查找
解释JavaScript中三个点的作用:数组/对象扩展运算符、rest运算符(使用函数的参数时,无论是完全替换参数还是与函数的参数一起替换参数,这三个点也称为rest运算符)
JavaScript怎么输出?输出方式有哪些?下面本篇文章就给大家介绍JavaScript的几种输出方式。window.alert()方法用于显示带有一条指定消息和一个【确认】 按钮的警告框。
for循环示例;让用户输入行数,使用for循环嵌套打出倒着的星星出来,行数等于用户输入的数字 ;有1,2,3,4这么4个数,能组成多少个互不相同且不含有重复数字的三位数?都是多少?
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!