为什么会有这么一个文章出现 ? 其实在写这篇文章的时候内心也是五味杂陈 :
"编译"
。词法分析 :
var name = "FruitJ";
会被分割为 : var
、name
、=
、FruitJ
、;
。语法分析 :
"不是正常人玩的东西"
】 ?
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "name"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "String",
"value": "\"FruitJ\""
},
{
"type": "Punctuator",
"value": ";"
}
]
目睹一下 AST 语法树的真容(以 var name = "FruitJ";
这句为例) :
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "name"
},
"init": {
"type": "Literal",
"value": "FruitJ",
"raw": "\"FruitJ\""
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
代码生成 :
经过这些步骤后, 就可以准备执行了 !
实际上代码在浏览器后台运行离不开三位老哥 :
JS 引擎【JS 代码的编译和执行的过程都是这位老哥一手督战的】
编译器【上面所说的编译时的那三个阶段就是编译器来完成的】
作用域【是一种规定了如何访问变量的一些列规则和约定 -> 如果这句话不明白没关系,在下文会具体解释】
。var name = "FruitJ";
来说 JS 引擎会处理两次, 编译时处理一次、运行时处理一次, 也就是在编译阶段
当编译器遇到了 var name = "FruitJ"
这句代码会去当前的作用域【作用域不是什么空间什么的不要混淆概念,下文会详述】中查找, 如果有则忽视该条声明, 如果没有就会重新声明这个变量, JS 引擎在运行阶段会去执行 name = "FruitJ"
这个赋值操作。所以 JS 引擎就会沿着作用域链去查找该变量,而使用的具体查找机制就是下面的 RHS
和 LHS
查询譬如说 :
=> RHS 查询在所有嵌套作用域中遍寻不到需要的变量,引擎就会抛出 ReferenceError 异常。
=> LHS 查询,如果在顶层作用域中也无法找到目标变量赋值的话,就会在全局作用域下隐式的创建一个具有该名称的变量,并将其返还给 JS 引擎。
=> 严格模式 如果是在严格模式下, 是禁止隐式的创建全局变量的,所以在严格模式下 LHS 查询失败后会抛出与 RHS 查询失败类似的 ReferenceError 异常。
=> 如果 RHS 查询到了一个变量,但是对该变量值进行了不合理操作就会抛出 TypeError 异常(就比如 -> undefined())。
LHS
: 不关心当前被赋值的目标是什么只想为将要赋的值找一个目标赋出去。在非严格模式下,如果一直到全局作用域中都找不到这个 “目标”,就会在全局作用域下声明一个,而在严格模式下就不会。a = 9 此时 a 就是一个 LHS 引用。RHS
: 要使用这个变量,当前作用域没有就像上查找,如果到了全局作用域中还没找到直接不开心的抛出异常。console.log(a); 此时 a 就是 RHS 引用。LHS
就是往出 "给"
, RHS
是往回 取
。x = 1;
console.log(y);
答案 : 第二行会抛出 ReferenceError 异常。
解析 : x = 1
就相当于进行了一次 RHS
查询, 在全局环境中没有找到, ok 创建一个好了。而 console.log(y);
是对 y 进行了一次 RHS
查询。结果全局环境下没有此变量直接抛出 ReferenceError
类型的异常。
a = (function add(num) {
return function(num) {
a = num
}
})(5);
a(6);
console.log(a);
答案 : 不会, 分析如下 :
a = (function add(num) { // 这里对 a 进行 LHS 查询, 当前上下文没有变量 a 所以在全局上下文下隐式的造了一个。同时对形参 num 进行 RHS 查询 -> num = 5
return function(num) { // 此处对 num 进行 RHS 查询, 上层函数的上下文中有 num (没问题)
a = num; // 对 a 进行 LHS 查询, 此时全局上下文中已经有全局变量 a ,这一步直接赋值,对 num 进行 RHS 查询, 当前上下文有 num 变量(没问题)。
}
})(5); // 对 add 进行 RHS 查询 -> add(5)
a(6); // 这里对 a 进行 RHS 查询, 此时 a 存储的引用地址存在(没有问题)
console.log(a); // 所有的 RHS 查询均正常找到对应变量,此刻又非是在严格模式下,故不会抛出异常, 运行代码 a 为 6。
a = (function add(num) {
var b;
return function(num) {
b = num;
}
})(5);
a(6);
console.log(b);
答案 : 会, 分析如下
a = (function add(num) { // 此处对 a 进行 LHS 查询, 当前上下文没有变量 a 所以在全局上下文下隐式的造了一个。同时对形参 num 进行 RHS 查询 -> num = 5
var b;
return function(num) { // 此处对 num 进行 RHS 查询, 上层函数的上下文中有 num (没问题)
b = num; // 对 b 进行 LHS 查询, 在上层函数的上下文中找到了 b ,故直接赋值, 对 num 进行 RHS 查询, 当前上下文有 num 变量(没问题)。
}
})(5); // 对 add 进行 RHS 查询 -> add(5)
a(6); // 这里对 a 进行 RHS 查询, 此时 a 存储的引用地址存在(没有问题)
console.log(b); // 对 b 进行 RHS 查询, 当前全局上下文中没有 b 变量, 故抛出 ReferenceError 异常
a = 12;
console.log(a); // 12
在非严格模式下不会抛出异常。
严格模式下 :
'use strict';
a = 12;
console.log(a); // Uncaught ReferenceError: a is not defined
在严格模式下抛出 ReferenceError 异常。
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
答案 :
function foo(a) { // 对 a 进行 LHS 查询 -> a = 2
var b = a; // 对 b 进行 LHS 查询, 对 a 进行 RHS 查询
return a + b; // 对 a, b 都进行一次 RHS 查询
}
var c = foo( 2 ); // 对 c 进行 LHS 查询, 对 foo 进行 RHS 查询
所以本题 : RHS 查询一共有 4 次, LHS 一共有 3 次。
执行上下文与作用域不是一个东西, 网络上有很多文章包括笔者以前都分不清二者之间的区别,一直认为二者就是一个东西, 现在看来并不是的。
作用域 : 实际上就是一套规则, 规定了访问变量时候的权限, 并对其行为进行严格限制,作用域只是一个“空地盘”,其中并没有真实的变量,但是却定义了变量如何访问的规则和定义了当变量访问不到的时候如何向上查询的一套规则。并不是我们想象中的作用域是一个 { }
, 也不是我们想象中的与执行上下文, 作用域就是一套规则, 一个抽象概念,仅此而已。 JS 引擎在运行阶段查找变量的时候也是通过作用域的辅助来进行的。
执行上下文 : 也可以叫做执行环境。这个执行环境通常是函数即将在进栈执行之前开始创建的,里面包含着活动对象。在函数执行完毕后, 通常如果在全局上下文中没有引用着该函数的变量,该函数的执行上下文将会出栈销毁, 当然里面的变量也一并销毁。如果被引用着呢 … 就会将该函数向栈底的方向压,给其他待执行函数提供执行空间。
所以二者的区别还是很大的, 包括有些时候总是能听到函数执行完毕作用域就被销毁了云云, 如果了解了这些就会知道这个表述是有问题的,你可以说执行上下文销毁了,但是不能说作用域被销毁,作用域在编译阶段就产生了,说的更夸张一些,在你写代码的时候就基本确定了(这就要涉及到下面的词法作用域了) !
基于这个知识, 在以后面对代码的时候如果涉及作用域,一定一定要站在它本来的角度去看待,去探索 !!!
当然不理解这些也没问题, 可以做对题也可以写代码, 但是笔者有洁癖 …emm
========== 2020-03-20 补充===============
还需要补充一点就是这个函数执行并不是把存在堆里面的实例整体的剪切过来执行, 只是创建了当前函数的执行上下文, 让这个执行上下文进栈执行, 而且这个执行上下文也可以创建多个 : 譬如说你同一个函数分别调用两次就会形成不同的执行上下文, 这一点传个不同参数体验一下即可, 虽然执行上下文可以有一个但需要注意的是作用域从始至终就只有一个, 还是那句话二者是有区别的 !
词法作用域
仅仅只是作用域的工作模型之一, 另一个工作模型是 动态作用域
。let name = "FruitJ";
function sayHello() {
console.log(`Hello ${ name }`);
}
function foo() {
let name = "XXY";
sayHello();
}
foo();
会输出 : "Hello FruitJ"
因为在 JavaScript 中作用域采取的是 词法作用域
这种工作模型, 作用域早在词法阶段就已经被确定好的。所以 sayHello 的上一级作用域就是全局作用域, 与 foo 的作用域没关系。
换句话说和函数在哪调用的没关系, 但是如果是动态作用域呢 ? 如果是动态作用域就会输出 "Hello XXY"
,因为动态作用域的标准就是与函数在哪调用有关。
eval
和 with
是可以 “欺骗词法作用域” 的, with 基本弃用主要说下 eval。function foo(str, a) {
eval( str ); // eval 大骗子!
console.log( a, b ); // 1, 3
}
var b = 2;
foo( "var b = 3;", 1 );
会输出 1 和 3 , 如果没有 eval 那么 b 应该是 2 。
上述提到过 JavaScript 是采用的词法作用域的工作模式,就是说 “一切均发生在被定义的时候”。但是 eval 这个臭小子把它骗了。
eval 可以接收一个字符串参数,并将其转换成程序代码来运行。
再看看下面这段代码是不是也 “欺骗了词法” 呢 ?
function foo(str, a) {
var b = 3;
console.log( a, b ); // 1, 3
}
var b = 2;
foo( "var b = 3;", 1 );
不是的, 因为在词法阶段确定了 foo 被定义在全局,同时自己本身已经定义了 b 了, 就不再去自己被定义的地方去找了,因为自己已经有了,但是 eval 那种情况不一样, eval 是在词法阶段,没有检测到 foo 内部是否有这个 b 只有到 eval 执行的时候才确定,所以说 eval 是个大骗子它会欺骗词法也同时改变机制,所以在工作时尽量少用 eval, 否则对代码的健壮性是个考验。
如果使用 eval 性能会怎样 ?
性能不怎么样,本来浏览器可以根据词法阶段的状态给出最优的优化方案,但一看到 eval 就犯愁不知道 eval 里面是个啥,贸然优化可能会导致结果不准确,所以就不做优化,对用户而言,不做优化就相当于性能退化!
VO
是 JS 引擎实现的,并不能由 JS 环境直接访问, 换句话说变量对象是在函数被调用, 但函数尚未执行的时刻创建的【执行上下文形成的时候】,而创建这个变量对象的过程就是( 函数参数、作用域链、内部变量、this 绑定、内部函数初始化的过程 )。AO
未进入执行阶段之前,变量对象中的属性都不可访问 , 这时候活动对象上的各种属性才能被访问, 但是进入执行阶段之后,变量对象就转换为了活动对象,里面的属性就都能被访问了,然后开始执行阶段的操作。作用域链的前端,始终都是当前执行的代码所
在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象 — 《JavaScript 高级程序设计第三版》
所以这个作用域链大概就是一个又一个的变量对象或是活动对象来组成的, 而作用域链的用途则是保证对执行上下文有权访问的所有变量和函数的有序访问。
而这个作用域链的最后一个对象则始终是全局执行环境的变量对象。
作用域链的向 上 / 后 逐级查找的这个机制叫做作用域链的回溯机制。
[[scope]]
[[ ]]
包裹的属性都是不可访问的, 但是我们可以在控制台看到他) :下面这段代码可以直观的帮助我们观察到作用链。
let a = 1212;
function fun() {
let b = 456;
var name = "FruitJ";
function foo() {
let c = 789;
var age = 22;
console.dir(foo);
function fn() {
var f = 99;
console.dir(fn);
}
fn();
}
foo();
}
fun();
this 是当前执行代码的环境对象, 或者说执行的每个 JavaScript 函数都有对其当前执行上下文的引用, 通俗理解就是,谁触发的我, 谁引用着我呢我就指向谁。
this 绑定的四种方式 :
function foo() {
console.log(this.a);
}
var a = 2;
foo();
在全局作用域下 foo() 是直接使用不带任何修饰的函数引用进行调用的所以 foo() 的 this 就指向了 window 。
var a = 2;
function fun() {
console.log(this.a); // 100
}
var obj = {
a: 100,
fun,
};
obj.fun();
虽然这个函数严格上来说并不是属于 obj 对象。但是当函数被调用时会使用 obj 的上下文来引用该函数, 所以 this 指向了 obj。
var a = 456;
function fun() { console.log(this.a); // 123 }
var obj = { a: 123 };
fun.apply(obj);
通过 apply、call、bind 方法可以改变函数的 this 指向使其指向参数一位置的实例。
function Person(name) {
this.name = name;
}
var alice = new Person("alice");
console.log(alice.name); // alice
也就是说 当我们使用 new 来构造函数调用的时候会自动执行下面的操作 :
__proto__
属性指向其构造函数的 .prototype
原型对象。因为 VO 和 AO 是 ES3 提到的概念已经算是比较老了, 而词法环境、变量环境是 ES5 提出来的说法。
先来看看各个权威来源对闭包的介绍 :
《红宝书》
: 闭包是指有权访问另一个函数作用域中的变量的函数。《你不知道的 JavaScript》
: 当函数可以记住并访问所在的词法作用域时,就产生了闭包, 即使函数是在当前词法作用域之外执行。闭包 - MDN
: 函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。闭包函数 - 百度百科
: 即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。闭包 - 百度百科
: 在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。笔者比较不认为可以将闭包视为一个函数, 笔者认为闭包应该是一个抽象的概念, 譬如说我们不能直接说某个函数是闭包, 而应该说某个函数在某种条件下形成了闭包。
对此, 对闭包这个概念的看法主要分两种 :
从理论角度 : 所有函数在创建的时候就将其上级的上下文的数据保存起来了, 即使是全局变量。这个时候每个函数都相当于是一个闭包。
从实践角度 :
-1). 即使创建它的上下文已经销毁, 它仍然存在。
-2). 在代码中引用了自由变量。
所以这两种看法不能说谁对谁错,也说不清楚。
在笔者看来对闭包的定义更倾向于 《你不知道的 JavaScript》
的表述。
function fun() {
var name = "FruitJ";
function foo() {
console.log(name);
}
foo();
}
fun();
实际上此函数就已经形成了闭包, 因为 foo 这个函数可以访问到自由变量 name
。
有人说闭包的形式必须是函数套函数, 实际上函数套函数的原因只是为了造出来一个自由变量。
也有人说闭包的形式是函数套函数再将内部函数返回, 实际上这段表述中确实形成了闭包, 至于需要将内部函数返回则无疑是为了使用 "闭包"
罢了。
实际上不仅仅非得 return , 直接暴露出去也可以 : 代码如下
return
function fun() {
var name = "FruitJ";
function foo() {
console.log(name);
}
return foo;
}
var f = fun();
f();
直接暴露
(function fun() {
var name = "FruitJ";
function foo() {
console.log(name);
}
window.foo = foo;
})()
foo();
这两种方式都成功的保存了引用。
var i = 5;
function fn(i) {
return function(n) {
console.log(n + (++i));
}
}
var f = fn(1);
f(2); // 4
fn(3)(4); // 8
fn(5)(6); // 12
f(7); // 10
console.log(i); // 5
global
对象和 script
对象通过一段代码我们就可以看到传说中的 global
对象长啥样以及
window 对象与 global 对象真正的关系以及鲜有耳闻的 script
对象。
global
对象, 笔者原来以为没有,以为浏览器只有 window 对象, 但事实并不是这样的, window 对象是被包含与 global
对象中的 , 只不过 global
对象平常手段是访问不到的。还有 script 对象, 这里面存储的都是在全局上下文用 let / const 声明的变量, 并且多个 script 域共用一个 script 对象。 <body>
<script>
let name = "FruitJ";
var action = "running";
function fun() {
let age = 22;
function foo() {
console.dir(foo);
}
foo();
}
fun();
script>
<script>
let hobby = "打代码";
target = "成为大神";
function fun() {
let age = 23;
function foo() {
console.dir(foo);
}
foo();
}
fun();
script>
body>
展开后 :
展开 global 对象(我们会看到我们使用非 let / const 声明的变量)。
找到 window 对象。
展开 window 对象。
在 global 和 window 对象里我们都分别找到了我们使用非 let / const 声明的变量。
写到这里终于算是写完了, 以上就是最近搜集整理并且稍稍理解一点的东西, 感觉好像还是没有把该说的该表达的讲清楚。目前功力暂时就是这些了, 如果本篇文章可以扫清你的一些疑虑那是再好不过了, 因为那正符合本文的主旨 ! 如果本文给你带来的疑虑增加了甚至没有减少一些那将是笔者的失败,没有更好的去理解和阐述,希望在下方留言区进行指正,笔者将不胜感激 !!!