开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
涅槃计划JS篇
前言
hi大家好,我是小鱼,接下来准备和大家一起梳理JavaScript的底层知识。这些可能大家都已经系统的学过,其中肯定也会遇到很多晦涩难懂的概念,路漫漫其修远兮,我开卷了,你们随意。今天复习的是作用域和作用域链,理解了作用域对理解闭包,执行上下文等概念都有很大的帮助,且看我们如何制裁它们。
作用域
作用域(scope)是指程序源代码中定义变量的区域。字面意思理解,变量在属于它自己的区域才会有作用,而限定这个变量的作用范围就叫做作用域。
因为JavaScript采用的是词法作用域(静态作用域),所以函数的作用域在函数定义的时候就决定了。那么相对的还有一个叫动态作用域,动态作用域是指函数的作用域是在函数调用的时候才决定的。
栗子
var num = 1;
function A() {
console.log(num); // ?
}
function B() {
var num = 2;
A();
}
B();
结果是:1
因为JavaScript采用的是静态作用域,在执行 A 函数时,会先从 A 函数内部查找是否有局部变量 num,如果没有,就根据书写的位置,查找上面一层的代码,也就是全局的 num 等于 1,所以结果是 1。
如果是动态作用域,执行 A 函数,依然是从 A 函数内部查找是否有局部变量 num。如果没有,就会从调用函数的作用域,也就是 B 函数内部查找 num 变量,所以结果会打印 2。
静态作用域分类
全局作用域
在浏览器中,全局作用域就是window对象。拥有全局作用域的变量可以在所有作用域中被访问,直接上代码解释↓
1. window下的属性
var fruit = 'apple'
function category() {
console.log(fruit)
}
category() // apple
可以看到最后是打印了 apple ,即使category内部函数里面没有声明 fruit 变量。说明category可以访问到拥有全局作用域的变量 fruit
2. 未定义直接赋值的变量
fruit = 'peach'
function category () {
remarks = '油桃好吃'
console.log(fruit, remarks)
}
category() // peach 油桃好吃
调用 category 也是正常打印了,没有报错是因为 fruit 被定义在了全局作用域,即使没有用 var 声明也会被挂在window对象上,相当于 window.fruit = 'peach'。
局部作用域
局部作用域一般只在固定的代码片段内可访问到。
1. 函数作用域
定义在函数中的变量就处于函数作用域中。不同函数作用域中,变量不能相互访问。
function category() {
var remarks = '油桃好吃'
}
function nums() {
console.log(remarks); // Uncaught ReferenceError: remarks is not defined
}
nums()
console.log(remarks) // Uncaught ReferenceError: remarks is not defined
可以看到在 nums 内部和全局打印 remarks 都报错了,此时的变量 remarks 在 category 函数作用域中,其他作用域调用就会报错,因为内层作用域可以访问外层作用域,但是外层作用域不能访问内层。
2. 块级作用域
ECMAScript 6 (简称ES6) 中新增了块级作用域。
块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。这一块比较简单,就不多嘴了嘿嘿,简单列一下 var、let、const 的区别
var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
在分析作用域链之前先了解一下JavaScript引擎是如何去编译的
JavaScript引擎
先看下面这一段代码
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
打印的结果是两个 foo2,是因为 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”。
到底JavaScript引擎遇到一段怎样的代码时才会做“准备工作”呢?
这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了,其实很简单,就三种,全局代码、函数代码、eval代码。
举个栗子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,其实就叫做"执行上下文(execution context)"。
执行过程
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
- 进入执行上下文
- 代码执行
栗子↓
var fruit = 'pear';
function foo(name, remarks) {
console.log(type);
console.log(name);
function name() {};
console.log(name);
var num = 10;
function num() {};
console.log(num);
var type = function() {};
}
foo(fruit);
1. 进入执行上下文
创建活动对象,找函数的所有形参,函数声明和变量声明
- 函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined
- 函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 变量声明
- 由名称和对应值(undefined)组成一个变量对象的属性被创建;
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
根据规则 分析AO
AO = {
// 1. 找形参和变量声明,然后将形参和变量声明作为AO的属性名,没有实参,属性值设为 undefined
name: 'pear',
remarks: undefined,
num: undefined,
type: undefined,
// 2. 找函数声明,如果变量对象已经存在相同名称的属性,则完全替换这个属性
name: reference to function name(){},
remarks: undefined,
num: reference to function num(){},
type: reference to FunctionExpression 'type',
}
2. 代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
分析AO
AO = {
name: function(){},
remarks: undefined,
num: 10, // 函数声明的优先级会高于变量声明,提升完以后,赋值num = 10
type: function(){},
}
依次打印:
// undefined
// ƒ name() {}
// ƒ name() {}
// 10
作用域链
对每个函数而言,都有自己的执行环境,而作用域链是与执行环境相关的。在 JavaScript 中,“一切皆对象”,而函数的这个对象有一个内部属性 [[scope]] ,该内部属性指向了该函数的作用域链,而作用域链中存储了每个执行环境相关的变量对象。
-
每当创建(或声明)一个函数的时候,那么会创建这个函数的作用域链,就会保存所有父变量对象到其中。
-
当函数被调用时,进入函数上下文,创建 GO/AO 后,就会将活动对象添加到作用链的顶端,而之前的对象就被压在了新的变量对象的下边,这个可以类比栈。
我们来梳理一下函数执行上下文中作用域链的创建过程:
栗子↓
var scope = "global";
function bar(){
var color = 'yellow';
function fn(){
var size = 'l';
}
console.log(color);
}
bar();
执行过程↓
// 1.bar 函数被创建,保存作用域链到 内部属性[[scope]]
bar.[[scope]] = [
globalContext.GO
];
// 2.执行 bar 函数,创建 bar 函数执行上下文,bar 函数执行上下文被压入执行上下文栈
ECStack = [
barContext,
globalContext
];
// 3.bar 函数并不立刻执行,开始做准备工作,复制函数[[scope]]属性创建作用域链
barContext = {
Scope: bar.[[scope]],
}
// 4.初始化活动对象,加入形参、函数声明、变量声明
barContext = {
AO: {
color: undefined
fn: undefined
},
Scope: bar.[[scope]],
}
// 5.将活动对象压入 checkscope 作用域链顶端
barContext = {
Scope: [AO, [[Scope]]]
}
// 6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
barContext = {
AO: {
color: yellow
fn: function(){}
},
Scope: [AO, [[Scope]]]
}
// 7.查找到 color 的值,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
总结
-
作用域链存储的就是执行上下文的集合。
-
当前作用域中没有使用未在在作用域定义的变量时,会沿着作用域链向上找,这样由多个执行上下文的变量对象构成的链表就叫做作用域链。