词法效果域 和 动态效果域

咱们常说js中效果域(大部分状况)在界说的时分就确认了,而this指向在运转的时分才干确认,了解这句话需求搞清楚编译阶段和运转阶段到底在干啥。

词法剖析:这个进0 U k v程浏览器g $ 4 1 / S T @o z 7 4 A n W把咱们写好的代码处理成对核算机来说有意义的词法单元,例如 var a = 1 通常会7 j q L x被分解成以下词法单元 var、a、=、1;

语法剖析:这个进程浏览器将词法单元调集转换成语法结构树,也便是笼统语法树AST;

代码生成:这个进程浏览器将AST转换成可履行代码;

经过上面三个阶段,浏览器现已能够运转咱们得到的可履行代码了,这整1 ! n个进程便是编译阶段,后边临可履行代码的运转便是t a R转阶段

在编程语言中效果域分为两种词法效果域动态效果域,来看这两种效果域有啥子区别。

词法效r r l i M L 8果域4 w f – ` 3:词法* | O @ q U t k R效果域依据编码结构确认效果域,效果域在编译的词法剖析阶段就确认了,在运转阶段s _ 8 L h ] A 0不再改动。

动态效果域:动态效果域在运转阶段确认,也便是说动~ A K [态效果域会依据不同的代码X F ^ [ v r S 5运转上下文发生变化。

js中效果域采用了词法效果域机制,但同时供给了一些办法v x & [ * x能够动态改动效果域,这也是J f L u前文说 “js中效果域(大部分)状况在界说的时分就确– W 5 $ * : ) q认” 的原因,尽管js有动态改动效果域的能力,但这并不代表js具有动态效果域的机! Y G x P w t H制,在语言特性上js便是静态效果域。

有些文章中说到js能够动态改动效果域就认为具有动态效果域机制,笔者认为这是不谨慎的。

经过一段DEMO了解静态效果域和动态效果域到底有什么区别

var a = 2
function foo()Z - M % } P y x | {
console.log(a)
}
functi_ p Mon baJ E m C . =r() {
var a = 3
foo(J ( H)
}
bar() // 2

deom.js
bar 调用,bar 里边 foo 被调用[ T w ~ D,foo 函数需求查找变量 a,因为js是词法效果域(即静态效果域),在编译阶段 foo 中没有找到变量 a,依据效果域查找规矩找到外层的 a=2,整个进程和运转无关。

deom.bash
常见的动态效果域语言比Z I z = . 3方bash,在bash中 bar 调用,bar 里边 foo 被调用,foo 函数需求查找变量 a,因为bash是动态效果域,在运转阶段查找到了 bar()( 9 2 u ~ 2 e ( 4 函数中的 a=3,整个进程和编译无关。

js中的词法效果域

function效果域

js中的效果域首先会想到function,一个function便是一个效果域空间也叫部分效果域,部分效果域内的变量不答应在外部拜访,function很容易构成嵌套结构当嵌套之后在当时效果域中无法找到某个变量时,编译引擎就会在外层嵌套的效果域中持续查找(也便是父级效果域特别注意是界说时的父级效果域并非运转时),直到找到该变量,或抵达最外层的效果域也便是大局效果域停止。这个逐层向上的查找机制便是效果域链的查找规矩。

当抵达最外层效果域且没有找到该变量时,如果是赋值操作(LHS)严厉形式会A [ U – * O抛出反常,非严厉形式会在最外层(大局)6 4 n , G : Z & 1界说该变量,这Q x o C e y { d也是为什么在js中不建议运用未声明变量的原因;如果是取值操作(RHS)9 6 o 7不管是不是严厉形式都会J e U u直接抛出反常。

赋值操作(LHS Left-hand Side):函数界说、函数传参、变量赋值
取值操作(RHS Right-har p : 7 Q ) Vnd Side):^ Z u函数调用、变量取值

var a = 0, b = 0, c = 0, d = 0;
fun? { & l ,ction fun1(){
var a = 1, b = 1, c = 1;
function fun2(){
var a = 2, b =2
function fW d 7 q @ Oun3(){
var a = 3
console.log(a, b, c, d) // 3 2 1 0
}
fun3()
}
fun2()
}
fun1()

看一下最里边的 console 对 a, b, c, d 标识符是怎样进行查找的
a:fun3
b:fun3 -> fun2
c:fun3 -> fun2 -> fun1Z Q s
d:fun3 -> fun2 -> fun1 -> global

块级效果域

ES6 供给的 let、const 关键字声明的变量都会固定于块(效果域空间)中,一对花括号 {} 能够构成一个效果域空间,在一个效果域空间中经过 let、const 声明的变量在该效果越外是不行见的,咱们称之为块w * ; – Q o 1 1L H ^ O ! ] O ) Q果域。块级效果域对 vas 1 b 0 H d y `r function 关键字声明的标识符并不# 1 F q收效。

常见的 function、if、for 、try/catch 都会生成一个效果域空间,特别要注意的是 for 的小括号也是一个独立的效果域空间。

for(var a = 0;9 @ % y u _ q , a <U E # d; 2s a j L  y y ` ^; a++){}
console.log(a)  // 2
for(let b = 0; b < 2; b++){}
console.lc o z [ f O oog(b)  // err: b is not defined
if(true){
function fun(){}
var c = 1
lev ~ 9 mt d = 1
const e = 1
}
console.log(fun) // function fun(){}
console.log(c)   // 1
console.log(d)   // err: d is not& j & 9 ` 7 k 5 y defined
console.log(e9 V i S _ c)   // err: e is not defined

特别要注意的是 let、const 不答应重复声明,如果在同一个效果域空间声明重复的标识符会抛出反常,var 和 functL : 7 n O ~ | Aion 则不受此规矩约束。

var a = 1; let a = 1      // err
let a = 1; var a = 1      // err
co` 3 V U ) ! ! j ons} M 4 b 4 J , Y 6t a = 1; var a = 1    // err
const a = 1; let a = 1    // err
let a = 1; functino a(){} // err
var a = 1; var a = 2      // ok
var a = 1; function a(){} // ok

既然 var 和 functR b fion 能够l W K重复声明,那I b 8 klet a = 1; functino a(){}let a = 1; var a = 1 不应该报错呀,let 在前 var 在后 let 运转的时分 a 还没有被+ = C 8 D d u界说,真的没有被界说吗?当然不是,var 关节字是会M 3 7 ~ . {提高的,具体细节后边讲。

动态改动效果域

一般说来词法效果域在代p % ] 0 } # P W码编译阶段就现已确认,这种机制的优点是在代码运转的进程中能够预知变量查找规矩,提高代码运转效率提高功能。可是js也答应动态改动效果域,比方 eval() 和 with 关键字。O T T r 5 [ 6 U

eval():eval() 函数核算 JavaScript 字符串,并把它作为脚本代码来履行。

var a = 0
function foo(str, b){
eval(str)
conN [ ; = p 4 l `sole.log(a, b)
}i a R h W c m
foo('var a = 1', 2) // 1, 2
foo('var a = 6', 7) // 6, 7
// 严厉形式下 `eval()` 会产生自己的效果域无法修改效果域
function foo(str){
'use strict';
eval(str);
console.log(a);
}5 w S
foo('var a =2'); // err: a iU I W S * + V B Is not defined

with:with 语句用于设置代码在特定目标中的效果域。

var obj = {a: 1, b: 2}
withw X k L X N % W (obj) {
a = 'is_a',
b = 'is_b'
c = 'is_c'
}
console.log(obj, c)  // {a: 'is_a', b:` } p { ~ 'is_b'}, 'is_c'

w) X { V M 8 zith 常用! H Z P于对一个object的特点以及办法快速引用,当在目标中没有找到对应办法时会走V 4 _漏到大局比方上文的 c 。

wi| 0 Q * 7 C R } Nth 和 evat ~ h ) Q }l 实际上依据运转逻辑在运转阶段临d 7 % ^时创立了一个全新的词法效果域。

变量提高

在编译阶段要确认效果域t $ # C首先要做的便是找到一切变量的声明,并利用] s ;相关机制将它们相关起来(词法效果域确认的实质),h ) ; 5 b R ? ] u这样就需求把相关变量提早声明,js 中 var、let、const、function 都能够声明关键字他们各自的提高逻辑各有差异,这是变量提高的原因。下面来看变量提高的特点:

  • 经过var界说的变量提高,而let和const进行的声明不会提高。
  • 经过function: s _ ~ n W键字界说的函数会被提高,而函数表达式则不会提高。
  • var声明自身会被提高,但包含函数表达式在内的赋值操作并不会提高
  • 每个效果域都会进* V 9 J f行提高操作,声明会被提高到所在效果域的顶部
  • 函数function关键字提高的优先级要高于变量var关键字。
  • 如果变量或函数有重复声明会以终究一次声明为主。
conso2 H ] g zle.log(a) // undefined
console.log(b)i M ! // ey $  6 # Z mrr: b is not defined
console.loD o r l r y 3 Ng(c) /y V $ p F ./ err: Cannot access 'c' before initialization
var a_ v ] * 7 /  = 1
let b = 2
const c = 3
/*
经过var界说的变量会提高,而let和const进行的声明不会提高。
var声明自身会被提高,但包含函数表达式在内的赋值操作并不会提高。
*/
console.log(d) // undefined
console.log(e) //  e(){}
var d = function(){}
functiS # q K & con e(){}
/*
经过function关键字界说的函数会被提高,而函数表达式则不会提高。
*/
var f = 1
function fun(){
console.log(f) // undefined
var f = 2W E J r v
}
fun()
console.log(f) // 1
/*
每个E 2 Y M +效果域都会进行提高操作,声明会被提高到所在效果域的顶部。
*l s n } |/
console.log(g) // function g(){}
var g = 1
function g(){}
console.log(g) // 1
console.log(g) // function g(){}
fl u , d D Y z 3unction g(){}
var g = 1
console.log(g) // 1
/*
函数functionR 9 . c O键字提高的优先级要高于变量var关键字。
所以第一个console.log(g)打印出了g()办法,当代码持续= ^ w y H ? a N i向下运转遇到function g(){},g()函数现已声明过了不做任何处理,而是被 g=1 的赋值操作给掩盖,所今后边一个console.log(g)打印出了1。
*/
function h(){'1X R R 3'}
function h(){'2'}
var i = 1
var i = 2
console.log(h, i)  // function h(){'2'}, 2
/*
如果变量或函数有重复声明会以终究一次声明为主。
*/

上面说过 let、const 不答应重复声明,var、function 则答应重复声明,现在明k ~ ? U A F & )白了变量提高回过头看一下varw 8 P 7 @ ;的重复声明到底是怎样事儿。

var a = 1
var a = 2

_ o ~ X价于

var a;
a = 1
a = 2

var 之所以能够重复声明是因为在编译阶段做了变量提高,到了运转阶段只是对a的重复赋值操作 ~

当提高遇到标识符重复的状况会按照以下逻辑处理:

  • 编译阶* ( | =段 var 关节字提高发现标识符重名不做任何操作,+ w Z Y ] w抛弃本次提高。
  • 编译阶段 fuD Q g ? y Q J lnction 关节字提高发现标识符重名会掩盖已有的标识符(function提高优先级高于var)。

效果域链

上面现已说过效果域链便是逐层向外的查找机制,直到大局,经过就近准则拿到标识符。可是一堆函数嵌套浏览器是怎样把他们的嵌套联系梳理清楚的?这个工作又是在哪个阶段完结的?

js的函数能够了解是一个目标,是 Function 目标的一个实a q y o Y例,FP P 3 n = r 9 5 QunctI i lion 目标和其他目– x W b o l标一样具有特点,比方 fun.name、fun.length 分别表明函数的姓名和参数长度,name 和 length 是可拜访特点,除此之外还有不行拜访特点比方 [[Scope]],不行拜访特点是给 JavaScript 引擎解析v % /读取的。其间 [[Scope]] 由ECMA-262规范第三版界说,包含了函p | L P s ~ H数的效果域调集,这个调集也被叫做函数的效果域链。效果域链以当时效果域为起点,逐渐向外终究在大局效果域完毕。

那么是不! N z Z o p是说效果域链能被咱们看到了?以上面的demo为例。

var a = 0, b = 0, c = 0, d = 0;
function fun1(){
var a = 1, b = 1, c = 1;2 F x g
function fun= { , ~ &2(){
var a = 2, b =2? c : X X Y
function fun3(){
var c Y f ~ a = 3
console.log(a, b, c, d)
}
fun3()
c} ? c F y G E M Aos L X q ] ^ U - vnsole.log(fun3.protoR . * f 5 W ( % Xtype)
}
fun2()
}
fun1()
[js深入理解] 作用域一系列问题 🏍🛩 🚗

[[Scope]] 在词T F ! P $ ~ w %法解析阶段(编译阶段` 0 8 h 6 h ] G)就现已确认了,今后效果域链屡不清楚的时分能够直接打印出来看看,这样对效果域链的了– } w ~ p = L p n解就七七八八了。

效果域的应用

遵从最小露出准则

在项目编码中应该遵从最小露出准则,这个准则q t –是说咱们应该最小限度的露出必要内容,将私有变量都界说在部分效果域防止污染大局。函数能够产生自己的效果域,在jq年代经常能够看到一些插件的源码写在一个当即履行的回调函数中(IIFE)% ` , f N,便是为了防止污染大局。

IIFE是js早期的模块化实现方案具体可移步文档 最具体的前端模块化解决方案梳? / q g . F * r

闭包

什么是闭包) ` N:当一个函数内部回来另一个函数,回来的这个函数拜访了其父v [ . P * .函数内部的变量,回; @ h来的函数在最外层被履行便是一个闭包。

当函数在界说时的词法效果域外被拜访时,闭包能够让函数持续拜访界说时的词法效果域。

function fooF S 9 % 6 7 a() {
var a = 2
functP H m g Mion bah  i Wr() {
console.log(a)
}
rett c J & 0 }urn bar
}
var fun = foo()
fun() // 2

bar是在函数foo中界说,履行却在最外层的大局,但仍能够拜访界说时foo的词法效果域。

因为js中垃圾回收机制的效果,函数在履行完后会被销毁,释放内存空间,上面比如当 foo() 履行完结后因为闭包的存在会阻止垃圾回收机制对foo()函数的释放,因为闭包需求拜访foo()函数内部的a变量(仍然存在引用)。很多运用闭包可能会造成内g s M存走漏的问题,可是以现在浏览器的功能来看不必: H } a ? W +太纠结这个。

总结

现在总结一下上文的内容

效果域:效果域能够被大局或者部分Y J e N ? Y / F界说,用来确认当时代码对标识符(变量以及函数)的拜访权限,起到标识符隔离防止冲突的意{ Y q A k图。
效果域链:按照一定查找规矩逐层查找标识符。
闭包:当函数在界d ! V & M + ! D ^说时的词法效果域外被拜访时,闭包 1 9 ( 9 h 1 Q能够让函数持续拜访界说时的词法效果域。
变量提高:编译阶段要确认效果域首先要找到一切变量的声明,并利用相关机制将它们相关起来(词法效果域确认的实质),这样就需求把相关变量提早声明。
未声明变量:一切末界说直接赋值的变量主动声明到大局效果域下。

几个DEMO

demo1

for(3 7 X q 5 G Dvar i = 0; iz o ~ U 3 < 3; i++){
setTimeout(e => {T i h
console.log(i) // 3 3 3
}, 0)
}
// 解法1
for(var i = 0; i < 3; iz J : - s +++){
(k => {
setTimeout(e => {
console.log(k) // 0 1 2
}, 0)
})(i)
}
// 解法2
for(M % Y 3 E m R dvar i = 0; i < 3; i++){
let k = i
setTimeout(e => {
console.log(k) // 0 1 2
}, 0)
}
//( R X 解法3
for(let i = 0; i &lk = 0 # { zt; 3; i++){
s` Z j ` R 1 s KetTimeout(e => {
console.log(i) // 0 1 2
}, 0)
}

因为i变量是用var声明的,var不具备块级效果域n 6 # % S机制,会露出到大局当履行1 R S Hconsole操作的时分当时大局的i现已是3了。

解法1:经过一个当即履行函数d L a f s W(IIFE)每次循环都会创立一个独立的效果域,取值的时分在自己独立的效果域取值互不影响。

解法2:let 声明的 k 有部分效果域6 0 v g的机制& ( ) J e 0 *,每次循环都会生成e 4 ( e V一个新的块级效果域并把k固定到里边互不影响。

解法3:直接用 let 声明 i,每次循环都会生成一个效果域固定i,只不过这个效果域并不是在foE / r ( k u sr的花括号中,而是在fo– l w /r的小括号中,for循环小括号的效果域是花括号的上一级效果域,这样就生成了三个独立的效果域嵌套,依据效果域向上查找机制仍然能够得到咱们预期的结果。

demo2

var scope = "global scope";
fun* ; 2 ( -ct! 6 ` n ! T k O (ion checkscopeN ~ j m U J S /(){a b R 4 1 ,
var scope = "local scope";
function f(){
return sco` [ ppe;
}
return f();
}
check} ( ) D A s ^ - es$ H ,cope() // loca? S Z # # f Hl scope
var scope = "global scope";
function checD ?  c D ;kscope(){
v1 X n y H o ; +ar scope = "local scope";
function f(){
return scope;
}
return f;
}
ch U S !eckscope()(); /} M E/ local scope

词法效果域(静态效果域)和运转上下文无关,效果域在编译阶段确认。

demo3

var a = 1
function fn() {
console.log(a) // err 不答应在界说前运用
let a = 2
bar()
console.log(a) // 2
}
function bar() {
console.log(a) // 1
}
fn() // err 1 2

尽管大局声明晰a,可是在fn()效果域中也声明晰let a,在当时效果域现已查找到了 a,就不会触] F ^ _ 3 7 }发效果域链的向上查找机制,并且K = ]let声明的变量不答应重复,不答应提早调用。
如果把 let a = 2 改成 var a = 2 经过变量提高输出内容是 undefined 1 2 如下面代码O l 8

v$ q 7 I } t sar a = 1
function fn() {
console.log(a)
var a = 2p a d s h ! l
bar()
console.log(a)
}
function bar() {
console.log(a)
}
fn() // undefined 1 2

demo4

var a = 1
f1 D G N 8 p uunction fn() {
console.log(a) // 3
a = 2 // bar -> global LHS
}
a = 3
function bx C L /ar() {
console.log(a) // 2
}
fn()  // 3
bar() // 2

demo5

console.log(a()) // 2
var a = function b(){
console.log(1)
}
console.log(a()) // 1
function a(){
console.log(2)
}
console.log(a()) // 1
console.log(b()) // err

经过变量提高和下面代码等价,要注意function提高优先级高于var;var a = function b(){} 这种写法属于函数表达式,p ; I j d 2 d C EN A h效果域下并没有b()函数。

function a(){
console.log(2)
}
console.log(a()) // 2
v- ! /ar a = undefined
var a = function b(){
console.log(1)
}
console.m S T 7 Slog(a()) // 1
// function a(){
//   console.log(2)
// }
coC + ; m j c Y jnsole.log(a()) // 1
console.log(b()) // err

demo6

function test() {
console.log(a) // undx T H -efined
console.log(b) // err: b is not defined
console.log(c) // err: Uncauw P . lght Referq { u t 7 O b +enceError
var a = b = 1   //Z x + 等价于 varA - X a=j M N v } y t B1; b=1
let c = 1
}
test()
console; e C v q $ 5 $ v.log(b) // 1
console.log(a) // err: a is not defined

var a = bv ~ ; s = 1 等价于 var a=1; b=1 b 未界说直接运用会被声明到大局,因为没有 var 关键字所以不会提高,所以 console.log(j n Sb) 抛出的反常是变量未界说;
c 经过 let 界说,不会提高且不答应提早运用,所以抛出的反常是不答应在变量界说前运用。

demo7

var a = [];
for (var i = 0; iV 4 $ $ E * l , 4 <v R X p ! % 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]B L ^() // 10
var a = [];
for (let i = 0; iq 1 x % < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6]() // 6

同demo1

demo8

fv + Q &unction a(age) {
console.log(ag. 7 9 6 ~ . Ke)   // function age() {}
var age = 2G C ` s 7 3 t H
co& ) ( d V j % j 6nsole.log(age)   // 2
function age() {}
console.log(age)   // 2
}
a(1)

a() 函数效果域中有 形参age、变量age、办法age 在编译阶段函9 : n 6 m数提高的优先级最高(出现同名状况函数将会掩盖其他标识符和编译顺序无关)所以第一个 console.log(age) 是 function age() {};
当运转到 var age = 2 时 age 被 2 掩盖,后边的 console.log(age) 都是2。

demo9

var a = 1
function f() {
console.log(a)
if(false) {
var a = 2
}
}
f() // und_ - e O + L }efined

编译阶段并不重视 if– 9 # – g b o % 语句有没有履行,都会把 var a 提高到当时效果域顶端。