系列文章:
- 执行上下文 + 执行上下文栈【JS深入知识汇点1】
- 作用域链【JS深入知识汇点2】
- JavaScript进阶2--this
- JavaScript进阶3--类型和值
- JavaScript进阶4--对象原型
- JavaScript进阶5--异步
作用域介绍
作用域是什么?
作用域是指程序源代码中定义变量的区域,本质上是一套规则,用于确定在何处以及如何查找变量(标识符)。
作用域类型
主要有两种:
- 动态作用域:是在代码运行时确定的,关注函数从何处调用。javascript 并不具有动态作用域,但是this机制某种程度上很像动态作用域。
- 词法作用域:在函数定义时决定了,关注函数在何处声明。有时候可能会有在代码运行时“修改”词法作用域的需求,可以通过以下机制:
-
eval():可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
function foo(str, a) { eval(str); console.log(a, b) } var b = 3; foo("var b = 4", 2); // 2, 4 -
with:通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域。
function foo(obj) { with(obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 }; foo(o1); console.log(o1.a) //2 foo(o2) console.loh(o2.a) //undefined; console.log(a) //2 在o2中,对a进行LHS引用,没有找到, //在o2中不会创造a这个属性 //因为是非严格模式,所以会在全局作用域中创建一个变量 a,并赋值给2⚠️注意:这两个机制只在非严格模式下有效,严格模式下会抛出 Reference 错误。还会导致性能下降,引擎无法在编译时对其进行优化,所以会变慢。
-
JS采用词法作用域,也就是静态作用域。词法环境是一种持有标识符-变量映射的结构。
词法环境的内部有两个组件:
- 环境记录器,存储变量和函数声明的实际位置
- 外部环境的引用,意味着可以访问其父级词法环境
作用域的种类
作用域有三种:
- 全局作用域:生命周期存在于整个程序内,能被程序中任何函数或者方法访问,在js中默认是可以被修改的。没有外部环境引用的词法环境。
- 局部作用域
- 函数作用域:函数作用域内,对外是封闭的,从外层的作用域无法直接访问函数内部的作用域。
- 块级作用域:任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的。
作用域链
作用域链是什么?
当查找变量时,会先从当前 EC 的 VO 中查找,如果没找到,就会去父级 EC 的 VO 中查找,一直到全局VO。这样由多个EC的VO构成的链表就是作用域链。

作用域链代码化
function foo() {
function bar() {}
}
// 函数创建时,各自的[[scope]]
foo.[[scope]] = [globalContext.VO]
bar.[[scope]] = [fooContext.AO, globalContext.VO]
// 函数激活时,会创建执行上下文,先创建 AO 对象,然后将 AO 对象插入到 [[scoped]]属性的链表的底部,组成新链表
foo.[[scopeChain]] = [fooContext.AO, globalContext.VO]
bar.[[scopeChain]] = [barContext.AO, fooContext.AO, globalContext.VO]
如何查找变量?
有以下两种方式:
- LHS:赋值操作的目标是谁;结果不成功的话,有两种情况:
- 严格模式下:抛出 Reference 异常。
- 非严格模式下,自动隐式地创建一个全局变量。
- RHS:谁是赋值操作的源头;结果不成功会报 Reference 异常。
⚠️注意:只会查找一级标识符,比 如foo.bar.baz,只会试图找到 foo 标识符,找到后,对象属性访问规则后分别接管对 bar、baz 的属性访问。
举🌰:
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(3);
引擎:作用域,我需要为 b 进行 LHS引用,这个你见过吗?
全局作用域:见过见过!刚才编译器声明它来着,给你。
引擎:谢谢大哥,现在我要把2赋值给 b
引擎:作用域啊,还有个事,我要对 foo 进行 RHS 引用,你见过没啊?
全局作用域:见过呀,它是个函数,给你。
引擎:好的,我现在执行一下 foo
引擎:哥啊,我需要对 a 进行 LHS 引用,这个你见过没?
全局作用域:这个也见过,是编译器把它声明成 foo 的一个形参了,拿去吧。
引擎:太棒了,现在我把3赋值给 a 了
引擎:foo 作用域啊,我要对 console 进行 RHS 引用,你见过没啊?
foo作用域:这我也有,是个内置对象,给你
引擎:你总是那么给力,现在我要看看这里有没有 log(),找到了,是个函数。
引擎:哥,我要对 a 进行 RHS 引用,虽然我记得好像有这个值,但是想让你帮我确认以下。
foo作用域:好,这个值没变过,你拿走吧。
引擎: 哥,我还要对 b 进行 RHS 引用,你找找呗
foo作用域:我没听过啊,你问问我的上级吧:
引擎:foo 的上级作用域兄弟,你见过 b 没啊?
全局作用域:见过 b 啊,等于2,拿走不谢!
引擎:真棒,我现在把 a + b ,也就是5,传递进 log(...)
难题解析:
Q1:
var a = 1
function fn1(){
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
fn1()() //2
结果是 2,因为:console.log 执行时,需要对 a 进行 RHS,在 fn3 中找 a 没找到,就去它的父级词法作用域中找,也就是fn1,就找到了 a = 2
Q2:
function Foo() {
getName = function () {
console.log(1);
};
return this;
};
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName(); //2
getName(); // 4
Foo().getName(); //1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); //3
Q3:
let x = 1;
function A(y){
let x = 2;
function B(z){
console.log(x+y+z);
}
return B;
}
let C = A(2);
C(3); //7
// 因为:在 B 里找 z 是3,找 y 没找到,去父级找,是2,x 找是2
Q4: 点的优先级大于等号的优先级
var a = {n: 1};
var b = a;
// 虽然赋值应该从右到左,但 . 的优先级比 = 高,
// 所以先执行 a.x = undefined;
// a = {n: 2},a的引用改变,指向新对象
// 之后执行 a.x = {n: 2}的时候,并不会重新解析a,
// 而是沿用最初解析a.x时候的a,就对象变成{x: {n: 2}, n: 1}
a.x = a = {n: 2};
console.log(a.x) // undefined
console.log(b.x) // {n: 2}
Q5:
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
// 1.调用 push 时,会在调用对象的key = length的地方做赋值,不管前面key是否有值
// 2.push方法如果对象有length属性,length属性会+1并且返回
// 3.对象如果同时满足:
// 1.有 splice 方法 2. length属性为正整数 => 输出会转换为类数组
obj.push(1)
obj.push(2)
console.log(obj)
// [2: 1, 3: 4, length: 4, push: f, splice: f]
Q6:
var b = 10;
// IIFE 的函数是函数表达式,而不是函数声明
// 函数表达式的函数名只在函数内部有效,并且是常量绑定
// 如果对一个常量进行赋值,在 strict 模式赋值,非 strict 模式静默失败
(function b() {
b = 20;
console.log(b) // function () {...}
console.log(window.b); // 10
})()
Q7: 简单改造下面的代码,使之分别打印 10 和 20。
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
// 输出10:
var b = 10;
(function b(b) {
window.b = 20;
console.log(b) // 10
})(b)
var b = 10;
(function b(b) {
b.b = 20;
console.log(b) // 10
})(b)
// 输出20:
var b = 10;
(function b() {
var b = 20;
console.log(b) // 20
})()
var b = 10;
(function (){
b = 20;
console.log(b);
})();
Q8:
var a = 10;
(function () {
console.log(a) //undefined
a = 5
console.log(window.a) // 10
var a = 20;
console.log(a) // 20
})()
Q9:
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a(); // 4
let obj = new Foo();
// 有直接方法a,不需要访问原型链
obj.a(); // 2
// Foo方法里替换了全局 Foo 上的 a 方法
Foo.a(); // 1
Q10: var 没有块作用域,声明会提升
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
// Goodbye Jack
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
let name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
// Hello Tom
var name = 'Tom';
(function() {
if (typeof name == 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
// Hello Tom