前言
作用域是JavaScript核心知识点之一,它充分体现了代码运行机制,搞清楚作用域可以提高代码质量减少问题的出现。
什么是作用域
概念:
用来声明,访问和修改变量的上下文,定义了变量的访问权限和查找机制。
作用域分类:
- 全局作用域 (整个JS运行环境,最顶层作用域,其声明的函数、变量等都是全局的)
- 函数作用域 (函数执行时会创建作用域)
- 块级作用域 ({ }大括号在 let、const关键字特性产生的作用域)
JS属于编译语言,逐行执行;编译的过程分为三部分:
- 分词/词法分析。
- 解析/把词法分析转换成AST(抽象语法树)。
- 代码生成/把AST转成可执行代码。
示例:
var a = 1;
编译过程:
- 分成var a、a=1;两部分进行分析。
- 查看当前作用域是否有a,如果有就忽略,如果没有就创建变量a。
(var、function声明的变量会在当前作用域下进行变量提升) - 赋值操作,首先查看当前作用域下是否有变量a,要是不存在变量a就会报错,要是存在进行赋值;其次如果是作用域嵌套的情况,当前作用域下不存在变量a,就会向外层作用域查找,直到全局作用域,如果不存在变量a就会报错。
执行过程:
- 执行var a语句进行查找a变量,这个过程叫做LHS(左侧为查找目标)。
- 执行a = 1赋值操作,过程叫做RHS(右侧为目标查找的目的)。
- 进行RHS必然会进行LHS。
注意:取值和赋值都是RHS,变量声明和形参是LHS;RHS和LHS发生在执行过程中。
示例:
function foo(a) {
var b = a;
return a + b
}
var c = foo(2)
3处LHS查询:
- var c 声明
- var b 声明
- 形参 a 声明
4处RHS查询:
- foo(2) 取值foo并执行
- var b = a语句,取值a
- a + b语句,取值a
- a + b语句,取值b
作用域嵌套:
当一个块或函数嵌套在另一个块或函数里,就发生了作用域嵌套。
作用域嵌套下变量的查找规则:
查找变量时如果当前作用域里没有找到,就会向外层作用域查找,直到找到该变量或到全局作用域为止,如果没找到就会报错。
作用域嵌套查找变量的特点:
从内往外,LHS和RHS都会在当前作用域进行,LHS只有当前作用域下没有找到所需变量,才会向外层作用域查找。
变量提升:
在编译阶段会对所有的var、function关键字声明的变量进收集,把收集的变量移动到当前作用域的最顶层,这个过程叫做变量提升。
特点:
- 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
- 每个作用域都会对var、function关键字声明的变量进行提升。
- 函数声明赋值给var声明的变量,这样的函数表达式只有var变量会提升,函数声明不会提升。
- 变量提升发生在编译阶段,赋值、取值等其他运行逻辑发生在执行阶段。
- 函数和变量重名,提升之后函数优先,会以函数为主。
提示:
1. var、function关键字在全局作用域下声明的变量是全局变量,同时还是全局作用域的属性;而不用var声明的变量只是全局作用域下的属性。
2.var在函数作用域下声明的变量只是当前作用域的私有变量,并不是函数的属性。
/**
* var fn进行提升,值为undefined。
* function boo 不会进行提升,因为boo成为了赋值操作表达式的一部分。
*/
console.log(fn) //undefined
console.log(fun) //ReferenceError: boo is not defined
var fn = function fun() {
console.log(1)
}
变量提升对LHS的影响:
- 变量提升发生在编译过程,一个没有用var声明的变量不会进行变量提升,在该变量之前进行RHS,会报ReferenceError异常is not defined。
- 用var声明的变量会进行变量提升,提升到当前作用域的最顶部,其值是undefined,因此在变量前进行取值不会报错。
- 非严格模式下对没有用var关键字声明的变量语句之前进行RHS,报ReferenceError异常。如果是之后进行RHS,会先进行LHS,如果当前作用域还是全局作用域下都没有找到,会自动创建一个全局变量并返回,严格模式下LHR查询失败时,并不会创建一个全局变量并返回,报ReferenceError异常。
- 变量提升发生在编译过程,赋值/取值发生在执行过程。
示例:
/**
* 通常认为代码是逐行执行,而下面的代码输出的结果是2,造成这个原因有两方面:
* 1. var声明的变量会进行变量提升,变量a被提升到当前作用域最顶部,其值是undefined。
* 2. 当代码从上至下执行时,执行a=1语句,a进行了RHS会触发LHS,此时a因变量提升被初始化,
* LHS就查找到了变量a并进行了赋值,下一个语句var a;已经进行过了初始化和赋值,
* 因此最终得到结果是2。
*/
a = 1;
var a;
console.log(a) //2
变量的访问权限问题:
- 块级作用域里的变量外层作用域是无法访问,变量是指由let,const。
- 函数的形参和变量,内部函数是外层作用域无法访问,属于局部变量。变量是指由let,const,var,function声明的。
词法作用域
简单的说就是编写代码时变量、函数、块级作用域书写代码时的样子。代码写成什么样就是什么样。
示例:
/**
* 箭头函数自身没有this,那么this如何查找?
* 箭头函数里的this其实是定义时所在的上层作用域的this。
*/
<-- 此次是全局作用域,浏览器下全局作用域的this指的是全局对象window -->
setTimeout(() => {
console.log('这里的this是::', this) //window
}, 0)
/**
* 作用域嵌套,foo bar执行后会创建函数作用域,
* a、b、c的RHS依据作用域嵌套规则进行查找,console.log打印出结果。
* 函数无论在哪里调用,如何被调用,函数作用域都只由函数被声明时所处位置决定。
*/
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c) // 2 4 12
}
bar(b * 3)
}
foo(2)
提示:示例体现出了作用域的定义就是变量的查找机制。
理解词法作用域的作用:
更加直观的理解代码的结构及执行,结合各个API的语法特点,减少开发时的错误。
修改词法作用域:
方法名 | 参数 | 说明 |
---|---|---|
eval | String | 生成执行的代码并运行,生成的代码就好像在那个位置一样,严 格模式下无效。 |
with | Object | 产生新的作用域解析参数。 |
示例:
/**
* eval把传入的str当成代码执行,修改了当前的词法作用域,导致编译器误以为var b = 2好像就是在那个位置一样,
* console.log(b,a)执行对b进行RHS查找,发现了foo作用域里有b的声明就直接取值;引用了错误,
* 其实应该引用全局的b;因此eval并不安全。
*/
var b = 1
function foo(str, a) {
eval(str)
console.log(b, a) //2,3
}
foo('var b = 2', 3)
//严格模式下无效
var b = 1
function foo(str, a) {
"use strict"
eval(str)
console.log(b, a)//1,3
}
foo('var b = 2', 3)
/**
* with解析参数obj,在新创建的作用域下进行obj.a=2的操作。
*/
function foo(obj) {
with (obj) {
a = 2
}
}
var obj = {
a: 1
}
foo(obj)
console.log(obj.a) //2
/**
* with解析参数obj,obj没有a属性,获取一个对象不存在的属性得到的是undefined。
* a=2进行RHS,触发LHS查找,给全局对象添加了a属性。
*/
function foo(obj) {
with (obj) {
a = 2
}
}
var obj = {}
foo(obj)
console.log(obj.a) //undefined
console.log(a) //2
/**
* with解析参数obj,var a是当前新作用域下的私有变量,无法进行obj.a的赋值操作,同时外界无法访问。
*/
function foo(obj) {
with (obj) {
var a = 2
}
}
var obj = {
a: 1
}
foo(obj)
console.log(obj.a) //1
提示:eval、with语句应该避免使用,会破坏词法作用域同时得不到编译器的性能优化。
函数作用域
当声明一个函数时都会为其自身创建词法作用域,属于这个函数里的全部变量只能在自身函数作用域范围及其嵌套作用域内使用、复用,外界无法访问。
示例:
/**
* 函数里声明的变量只能在自身函数作用域范围及其嵌套作用域内里使用、复用,外界无法访问。
*/
function fun() {
var a = 1;
function funs() {
let b = 2
console.log(++a)
if (a > 1) {
console.log(b)
}
}
funs()
console.log(b)
}
fun()
提示:示例体现出作用域对变量的HSL是从里往外查找,私有变量的可视范围;同时说明了函数具有封装性。
函数封装性的意义:
可以根据需求向外界暴露API,同时避免私有变量与外界同名变量冲突。
示例:
var b = 5
function fun(a) {
var b = funs(a * 3);
function funs(b) {
return b - 1
}
console.log(b)
}
function funs(b) {
return b + 1
}
fun(2)
funs(b)
console.log(b)
函数声明与函数表达式的区别:
函数表达式可以是匿名的,函数声明则不可以省略函数名。
匿名函数的缺点:
- 匿名函数在栈追踪中不会显示有意义的函数名,不利于调试。
- 匿名函数引用自身时只能通过arguments.callee引用。
立即执行函数:
/**
* 立即执行函数 IIFE,把一个函数包括在()大括号里的函数表达式,
* IIFE可以是匿名的,如果有函数名,只能在当前立即执行函数体里使用函数名。
* @ globel 给IIFE传递参数(可选)
*/
(function (globel) {
var a = 1;
console.log(globel)
})(window)
提示:通常在第三方库中更多使用匿名函数对代码进行模块化,充分利用了函数的封装特性。
闭包特点:
- 函数作用域才会产生闭包。
- 一个函数的内部函数被被外界引用,导致当前作用域无法销毁,当前作用域可以重复使用。
提示:闭包核心其实就是不会被销毁的作用域
块级作用域:
let、 const声明的变量会产生暂存性死区绑定到当前的{…}或for循环条件块里,从而产生块作用域;
const表示常量不可修改,如果要试图修改一个基本类型的const声明的变量值会引发报错,而Object类型的值是修改其引用地址会报错。