You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MyModules.define("bar",[],function(){functionhello(who){return"Let me introduct: "+who;}return{hello: hello};});MyModules.define("foo",["bar"],function(bar){varhungry="hippo";functionawesome(){console.log(bar.hello(hungry).toUpperCase());}return{awesome: awesome};});varbar=MyModules.get("bar");varfoo=MyModules.get("foo");console.log(bar.hello("hippo");)// Let me introduct: hippofoo.awesome();// LET ME INTRODUCT: HIPPO
// bar.jsfunctionhello(who){return"Let me introduct: "+who;}exporthello;// foo.js// 仅从“bar”模块导入hello()importhellofrom"bar";varhungry="hippo";functionawesome(){console.log(hello(hungry).toUpperCase(););}exportawesome;// baz.js// 导入完整的“foo”和”bar“模块modulefoofrom"foo";modulebarfrom"bar";console.log(bar.hello("rhino"));// Let me introduct: rhinofoo.awesome();// LET ME INTRODUCT: HIPPO
第1章 作用域是什么
1.1 编译原理
JavaScript语言是“动态”或“解释执行”语言,但事实上是一门编译语言。但它不是提前编译的,编译结果也不能在分布式系统中移植。
传统编译语言流程中,程序在执行之前会经历三个步骤,统称为“编译”。
分词/词法分析(Tokenizing/Lexing)
将由字符组成的字符串分解成(对编程语言来说)有意义的代码块。
上面这段程序会被分解成以下词法单元:var、a、=、2、;。
空格是否会被当做词法单元,取决于空格在这门语言中是否有意义。
解析/语法分析(Parsing)
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的数。这个数被称作
抽象语法树
(Abstract Syntax Tree, AST)。以上代码的抽象语法树如下所示:
代码生成
将
AST
转换成可执行代码的过程。过程与语言、目标平台等相关。简单来说就是可以通过某种方法将
var a = 2;
的AST转化为一组机器指令。用来创建一个叫做a的变量(包括分配内存等),并将一个值存储在a中。1.2 理解作用域
1.2.1 演员表
1.2.2 对话
var a = 2;
存在2个不同的声明。1、编译器在编译时处理(
var a
):在当前作用域中声明一个变量(如果之前没有声明过)。2、引擎在运行时处理(
a = 2
):在作用域中查找该变量,如果找到就对变量赋值。1.2.3 LHS和RHS查询
L
和R
分别代表一个赋值操作的左侧和右侧,当变量出现在赋值操作的左侧时进行LHS
查询,出现在赋值操作的**非左侧
**时进行RHS
查询。retrieve his source value
,即取到它的源值上述代码共有1处LHS查询,3处RHS查询。
LHS查询有:
a = 2
中,在2
被当做参数传递给foo(…)
函数时,需要对参数a
进行LHS查询RHS查询有:
最后一行
foo(...)
函数的调用需要对foo进行RHS查询console.log( a );
中对a
进行RHS查询console.log(...)
本身对console
对象进行RHS查询1.3 作用域嵌套
遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没有找到,查找过程都会停止。
1.4 异常
ReferenceError
和作用域判别失败相关,TypeError
表示作用域判别成功了,但是对结果的操作是非法或不合理的。ReferenceError
异常。ReferenceError
异常TypeError
异常。(比如对非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性)1.5 小结
var a = 2
被分解成2个独立的步骤。var a
在其作用域中声明新变量a = 2
会LHS查询a,然后对其进行赋值第2章 词法作用域
2.1 词法阶段
词法作用域是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,所以在词法分析器处理代码时会保持作用域不变。(不考虑欺骗词法作用域情况下)
2.1.1 查找
作用域查找会在找到第一个匹配的标识符时停止。
遮蔽效应:在多层嵌套作用域中可以定义同名的标识符,内部的标识符会“遮蔽”外部的标识符。
全局变量会自动变成全局对象的属性,可以间接的通过对全局对象属性的引用来访问。通过这种技术可以访问那些被同名变量所遮蔽的全局变量,但是非全局的变量如果被遮蔽了,无论如何都无法被访问到。
词法作用域只由函数被声明时所处的位置决定。
词法作用域查找只会查找一级标识符,比如a、b、c。对于
foo.bar.baz
,词法作用域只会查找foo
标识符,找到之后,对象属性访问规则会分别接管对bar
和baz
属性的访问。2.2 欺骗词法
欺骗词法作用域会导致性能下降。以下两种方法不推荐使用
2.2.1 eval
eval(..)
函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。eval('var b = 3')
会被当做本来就在那里一样来处理。eval(..)
中所执行的代码包含一个或多个声明,会在运行期修改书写期的词法作用域。上述代码中在foo(..)
内部创建了一个变量b,并遮蔽了外部作用域中的同名变量。eval(..)
在运行时有自己的词法作用域,其中的声明无法修改作用域。setTimeout(..)
和setInterval(..)
的第一个参数可以是字符串,会被解释为一段动态生成的函数代码。已过时,不要使用new Function(..)
的最后一个参数可以接受代码字符串(前面的参数是新生成的函数的形参)。避免使用2.2.2 with
with
通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,这个对象的属性会被处理为定义在这个作用域中的词法标识符。
这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with所处的函数作用域中。
上面例子中,创建了
o1
和o2
两个对象。其中一个有a
属性,另一个没有。在with(obj){..}
内部是一个LHS引用,并将2赋值给它。o1
传递进去后,with声明的作用域是o1
,a = 2
赋值操作找到o1.a
并将2赋值给它。o2
传递进去后,作用域o2
中并没有a
属性,因此进行正常的LHS标识符查找,o2的作用域、foo(..)
的作用域和全局作用域都没有找到标识符a,因此当a = 2
执行时,自动创建了一个全局变量(非严格模式),所以o2.a
保持undefined。2.2.3 性能
eval(..)
或with
,它只能简单的假设关于标识符位置的判断都是无效的。因为无法在词法分析阶段明确知道eval(..)
会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with
用来创建词法作用域的对象的内容到底是什么。eval(..)
或with,所有的优化可能都是无意义的,最简单的做法就是完全不做任何优化。代码运行起来一定会变得非常慢。2.3 小结
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。
编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
有以下两个机制可以“欺骗”词法作用域:
eval(..)
:对一段包含一个或多个声明的”代码“字符串进行演算,借此来修改已经存在的词法作用域(运行时)。with
:将一个对象的引用当做作用域来处理,将对象的属性当做作用域中的标识符来处理,创建一个新的词法作用域(运行时)。副作用是引擎无法在编译时对作用域查找进行优化。因为引擎只能谨慎地认为这样的优化是无效的,使用任何一个都将导致代码运行变慢。不要使用它们
第3章 函数作用域和块作用域
3.1 函数中的作用域
属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
foo(..)
作用域中包含了标识符(变量、函数)a、b、c和bar。无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处的作用域。全局作用域只包含一个标识符:
foo
。3.2 隐藏内部实现
最小特权原则(最小授权或最小暴露原则):在软件设计中,应该最小限度地暴露必要内容,而将其他内容都”隐藏“起来,比如某个模块或对象的API设计。
b
和doSomethingElse(..)
都无法从外部被访问,而只能被doSomething(..)
所控制,设计上将具体内容私有化了。3.2.1 规避冲突
”隐藏“作用域中的变量和函数带来的另一个好处是可以避免同名标识符之间的冲突。
bar(..)
内部的赋值表达式i = 3
意外的覆盖了声明在foo(..)
内部for循环中的i。var i = 3
。var j = 3
。规避变量冲突的典型例子:
全局命名空间
第三方库会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
模块管理
任何库无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显示的导入到另外一个特定的作用域中。
3.3 函数作用域
上述函数作用域虽然可以将内部的变量和函数定义”隐藏“起来,但是会导致以下2个额外问题。
foo()
,意味着foo
这个名称本身”污染“了所在的作用域。foo()
调用这个函数才能运行其中的代码。上述代码包装函数的声明以
(function...
开始,函数会被当做函数表达式而不是一个标准的函数声明来处理。function
是声明中的第一个词foo
被绑定在所在作用域中,可以直接通过foo()
来调用它。foo
被绑定在函数表达式自身的函数中,而不是所在的作用域。(function foo(){ .. }
中foo
只能在..
所代表的位置中被访问,外部作用域不行。foo
变量名被隐藏在自身中意味着不会非必要地污染外部作用域。3.3.1 匿名和具名
上述是匿名函数表达式,因为
function()..
没有名称标识符。函数表达式可以匿名,但函数声明不可以省略函数名。
匿名函数表达式有以下缺点:
arguments.callee
引用行内函数表达式可以解决上述问题,始终给函数表达式命名是一个最佳实践。
3.3.2 立即执行函数表达式
立即执行函数表达式(IIFE,Immediately Invoked Function Expression)
匿名/具名函数表达式
第一个( )将函数变成表达式,第二个( )执行了这个函数
改进型
(function(){ .. }())
用来调用的( )被移进了用来包装的( )中。
当做函数调用并传递参数进去
解决
undefined
标识符的默认值被错误覆盖导致的异常将一个参数命名为
undefined
,但是在对应的位置不传入任何值,这样就可以保证在代码块中undefined
标识符的值真的是undefined
。倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去
函数表达式
def
定义在片段的第二部分,然后当做参数(这个参数也叫做def)被传递进IIFE函数定义的第一部分中。最后,参数def(也就是传递进去的函数)被调用,并将window传入当做global参数的值。3.4 块作用域
表面上看JavaScript并没有块作用域的相关功能,除非更加深入了解(with、try/catch 、let、const)。
上述代码中
i
会被绑定在外部作用域(函数或全局)中。上述代码中,当使用var声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。
3.4.1 with
块作用域的一种形式,用
with
从对象中创建出的作用域仅在**with
声明中**而非外部作用域中有效。3.4.2 try/catch
ES3规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch中有效。
当同一个作用域中的两个或多个catch分句用同样的标识符名称声明错误变量时,很多静态检查工具还是会发出警告,实际上这并不是重复定义,因为所有变量都会安全地限制在块作用域内部。
3.4.3 let
ES6引入了
let
关键字,可以将变量绑定到所在的任意作用域中(通常是{ .. }
内部),即let
为其声明的变量隐式地劫持了所在的块作用域。用
let
将变量附加在一个已经存在的的块作用域上的行为是隐式的,如果习惯性的移动这些块或者将其包含在其他的块中,可能会导致代码混乱。为块作用域显示地创建块。显式的代码优于隐式或一些精巧但不清晰的代码。
在if声明内部显式地创建了一个块,如果需要对其进行重构,整个块都可以被方便地移动而不会对外部if声明的位置和语义产生任何影响。
在let进行的声明不会在块作用域中进行提升
1、垃圾收集
click
函数的点击回调并不需要someReallyBigData
。理论上当process(..)
执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click
函数形成了一个覆盖整个作用域的闭包,JS引擎极有可能依然保存着这个结构(取决于具体实现)。2、let循环
for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
3.4.4 const
ES6引用了
const
,可以创建块作用域变量,但其值是固定的(常量)第4章 提升
var a = 2;
会被看成两个声明,var a;
和a = 2;
,第一个声明在编译阶段进行,第二个赋值声明会被留在原地等待执行阶段。上面这段程序中,变量标识符
foo()
被提升并分配给所在作用域,因此foo()
不会导致ReferenceError。此时foo
并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值),foo()
由于对undefined
值进行函数调用而导致非法操作,因此抛出TypeError
异常。4.1 函数优先
var foo
尽管出现在function foo()...
的声明之前,但它是重复的声明,且函数声明会被提升到普通变量之前,因此被忽略第5章 作用域闭包
5.1 闭包
bar()
在自己定义的词法作用域以外的地方执行。bar()
拥有覆盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用,不会被垃圾回收器回收bar()
持有对foo()
内部作用域的引用,这个引用就叫做闭包。baz
传递给bar
,当调用这个内部函数时(现在叫做fn
),它覆盖的foo()
内部作用域的闭包就形成了,因为它能够访问a。setTimeout(..)
持有对一个参数的引用,这里参数叫做timer,引擎会调用这个函数,而词法作用域在这个过程中保持完整。这就是闭包5.2 循环和闭包
i
的最终值。i
尝试方案1:使用IIFE增加更多的闭包作用域
尝试方案2:IIFE增加变量
尝试方案3:改进型,将
i
作为参数传递给IIFE函数5.2.1 块作用域和闭包
let
可以用来劫持块作用域,并且在这个块作用域中声明一个变量。for
循环头部的let
声明会有一个特殊的行为。变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。循环改进:
5.3 模块
模块模式需要具备两个必要条件:
立即调用这个函数并将返回值直接赋予给单例的模块标识符foo。
5.5.1 现代的模块机制
大多数模块依赖加载器/管理器本质上是将这种模块定义封装进一个友好的API。
使用上面的函数来定义模块:
5.5.2 未来的模块机制
在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样可以导出自己的API成员。
ES6模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)
import
:将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上module
:将整个模块的API导入并绑定到一个变量上。export
:将当前模块的一个标识符(变量、函数)导出为公共API附录A 动态作用域
this
机制某种程度上很像动态作用域。附录B 块作用域的替代方案
ES3开始,JavaScript中就有了块作用域,包括with和catch分句。
上述代码在ES6环境中可以正常工作,但是在ES6之前的环境中如何实现呢?
答案是使用catch分句,这是ES6中大部分功能迁移的首选方式。
B.1 Traceur
B.2 隐式和显式作用域
let
声明会创建一个显式的作用域并与其进行绑定,而不是隐式地劫持一个已经存在的作用域(对比前面的let
定义)。存在的问题:
let
声明不包含在ES6中,Traceur编译器也不接受这种代码B.3 性能
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
The text was updated successfully, but these errors were encountered: