更新:谢谢你们的支持,最近折腾了一个博客官网出来,方便你们系统阅读,后续会有更多内容和更多优化,猛戳这里查看前端
------ 如下是正文 ------git
JavaScript语言是“动态”或“解释执行”语言,但事实上是一门编译语言。但它不是提早编译的,编译结果也不能在分布式系统中移植。github
传统编译语言流程中,程序在执行以前会经历三个步骤,统称为“编译”。面试
分词/词法分析(Tokenizing/Lexing)编程
将由字符组成的字符串分解成(对编程语言来讲)有意义的代码块。数组
var a = 2;
复制代码
上面这段程序会被分解成如下词法单元:var、a、=、二、;。安全
空格是否会被当作词法单元,取决于空格在这门语言中是否有意义。性能优化
解析/语法分析(Parsing)前端工程师
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的表明了程序语法结构的数。这个数被称做抽象语法树
(Abstract Syntax Tree, AST)。数据结构
var a = 2;
复制代码
以上代码的抽象语法树以下所示:
代码生成
将AST
转换成可执行代码的过程。过程与语言、目标平台等相关。
简单来讲就是能够经过某种方法将var a = 2;
的AST转化为一组机器指令。用来建立一个叫作a的变量(包括分配内存等),并将一个值存储在a中。
var a = 2;
存在2个不一样的声明。
一、编译器在编译时处理(var a
):在当前做用域中声明一个变量(若是以前没有声明过)。
st=>start: Start
e=>end: End
op1=>operation: 分解成词法单元
op2=>operation: 解析成树结构AST
cond=>condition: 当前做用域存在变量a?
op3=>operation: 忽略此声明,继续编译
op4=>operation: 在当前做用域集合中声明新变量a
op5=>operation: 生成代码
st->op1->op2->cond
cond(yes)->op3->op5->e
cond(no)->op4->op5->e
复制代码
二、引擎在运行时处理(a = 2
):在做用域中查找该变量,若是找到就对变量赋值。
st=>start: Start
e=>end: End
cond=>condition: 当前做用域存在变量a?
cond2=>condition: 全局做用域?
op1=>operation: 引擎使用这个变量a
op2=>operation: 引擎向上一级做用域查找变量a
op3=>operation: 引擎把2赋值给变量a
op4=>operation: 举手示意,抛出异常
st->cond
cond(yes)->op1->op3->e
cond(no)->cond2(no)->op2(right)->cond
cond2(yes)->op4->e
复制代码
L
和R
分别表明一个赋值操做的左侧和右侧,当变量出如今赋值操做的左侧时进行LHS
查询,出如今赋值操做的**非左侧
**时进行RHS
查询。
retrieve his source value
,即取到它的源值function foo(a) {
console.log( a ); // 2
}
foo(2);
复制代码
上述代码共有1处LHS查询,3处RHS查询。
LHS查询有:
a = 2
中,在2
被当作参数传递给foo(…)
函数时,须要对参数a
进行LHS查询RHS查询有:
最后一行foo(...)
函数的调用须要对foo进行RHS查询
console.log( a );
中对a
进行RHS查询
console.log(...)
自己对console
对象进行RHS查询
遍历嵌套做用域链的规则:引擎从当前的执行做用域开始查找变量,若是找不到就向上一级继续查找。当抵达最外层的全局做用域时,不管找到仍是没有找到,查找过程都会中止。
ReferenceError
和做用域判别失败相关,TypeError
表示做用域判别成功了,可是对结果的操做是非法或不合理的。
ReferenceError
异常。ReferenceError
异常TypeError
异常。(好比对非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性)var a = 2
被分解成2个独立的步骤。
var a
在其做用域中声明新变量a = 2
会LHS查询a,而后对其进行赋值词法做用域是定义在词法阶段的做用域,是由写代码时将变量和块做用域写在哪里来决定的,因此在词法分析器处理代码时会保持做用域不变。(不考虑欺骗词法做用域状况下)
做用域查找会在找到第一个匹配的标识符时中止。
遮蔽效应:在多层嵌套做用域中能够定义同名的标识符,内部的标识符会“遮蔽”外部的标识符。
全局变量会自动变成全局对象的属性,能够间接的经过对全局对象属性的引用来访问。经过这种技术能够访问那些被同名变量所遮蔽的全局变量,可是非全局的变量若是被遮蔽了,不管如何都没法被访问到。
window.a
复制代码
词法做用域只由函数被声明时所处的位置决定。
词法做用域查找只会查找一级标识符,好比a、b、c。对于foo.bar.baz
,词法做用域只会查找foo
标识符,找到以后,对象属性访问规则会分别接管对bar
和baz
属性的访问。
欺骗词法做用域会致使性能降低。如下两种方法不推荐使用
eval(..)
函数能够接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
function foo (str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
复制代码
eval('var b = 3')
会被当作原本就在那里同样来处理。
eval(..)
中所执行的代码包含一个或多个声明,会在运行期修改书写期的词法做用域。上述代码中在foo(..)
内部建立了一个变量b,并遮蔽了外部做用域中的同名变量。eval(..)
在运行时有本身的词法做用域,其中的声明没法修改做用域。function foo (str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2;" );
复制代码
setTimeout(..)
和setInterval(..)
的第一个参数能够是字符串,会被解释为一段动态生成的函数代码。已过期,不要使用new Function(..)
的最后一个参数能够接受代码字符串(前面的参数是新生成的函数的形参)。避免使用with
一般被当作重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复“obj”
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
复制代码
with能够将一个没有或有多个属性的对象处理为一个彻底隔离的词法做用域,这个对象的属性会被处理为定义在这个做用域中的词法标识符。
这个块内部正常的var声明并不会被限制在这个块的做用域中,而是被添加到with所处的函数做用域中。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b : 3
}
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 很差,a被泄露到全局做用域上了!
复制代码
上面例子中,建立了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。eval(..)
或with
,它只能简单的假设关于标识符位置的判断都是无效的。由于没法在词法分析阶段明确知道eval(..)
会接收到什么代码,这些代码会如何对做用域进行修改,也没法知道传递给with
用来建立词法做用域的对象的内容究竟是什么。eval(..)
或with,全部的优化可能都是无心义的,最简单的作法就是彻底不作任何优化。代码运行起来必定会变得很是慢。词法做用域意味着做用域是由书写代码时函数声明的位置来决定的。
编译的词法分析阶段基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它们进行查找。
有如下两个机制能够“欺骗”词法做用域:
eval(..)
:对一段包含一个或多个声明的”代码“字符串进行演算,借此来修改已经存在的词法做用域(运行时)。with
:将一个对象的引用当作做用域来处理,将对象的属性当作做用域中的标识符来处理,建立一个新的词法做用域(运行时)。反作用是引擎没法在编译时对做用域查找进行优化。由于引擎只能谨慎地认为这样的优化是无效的,使用任何一个都将致使代码运行变慢。不要使用它们
属于这个函数的所有变量均可以在整个函数的范围内使用及复用(事实上在嵌套的做用域中也可使用)。
function foo(a) {
var b = 2;
// 一些代码
function bar() {
// ...
}
// 更多的代码
var c = 3;
}
复制代码
foo(..)
做用域中包含了标识符(变量、函数)a、b、c和bar。不管标识符声明出如今做用域中的何处,这个标识符所表明的变量或函数都将附属于所处的做用域。
全局做用域只包含一个标识符:foo
。
最小特权原则(最小受权或最小暴露原则):在软件设计中,应该最小限度地暴露必要内容,而将其余内容都”隐藏“起来,好比某个模块或对象的API设计。
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15
复制代码
b
和doSomethingElse(..)
都没法从外部被访问,而只能被doSomething(..)
所控制,设计上将具体内容私有化了。
”隐藏“做用域中的变量和函数带来的另外一个好处是能够避免同名标识符之间的冲突。
function foo() {
function bar(a) {
i = 3; // 修改for循环所属做用域中的i
console.log( a + i );
}
for (var i = 0; i < 10; i++) {
bar( i * 2 ); // 糟糕,无限循环了!
}
}
foo();
复制代码
bar(..)
内部的赋值表达式i = 3
意外的覆盖了声明在foo(..)
内部for循环中的i。
解决方案:
var i = 3
。var j = 3
。规避变量冲突的典型例子:
全局命名空间
第三方库会在全局做用域中声明一个名字足够独特的变量,一般是一个对象,这个对象被用做库的命名空间,全部须要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将本身的标识符暴露在顶级的词法做用域中。
模块管理
任何库无需将标识符加入到全局做用域中,而是经过依赖管理器的机制将库的标识符显示的导入到另一个特定的做用域中。
var a = 2;
function foo() { // <-- 添加这一行
var a = 3;
console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2
复制代码
上述函数做用域虽然能够将内部的变量和函数定义”隐藏“起来,可是会致使如下2个额外问题。
foo()
,意味着foo
这个名称自己”污染“了所在的做用域。foo()
调用这个函数才能运行其中的代码。解决方案:
var a = 2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
复制代码
上述代码包装函数的声明以(function...
开始,函数会被当作函数表达式而不是一个标准的函数声明来处理。
function
是声明中的第一个词foo
被绑定在所在做用域中,能够直接经过foo()
来调用它。foo
被绑定在函数表达式自身的函数中,而不是所在的做用域。(function foo(){ .. }
中foo
只能在..
所表明的位置中被访问,外部做用域不行。foo
变量名被隐藏在自身中意味着不会非必要地污染外部做用域。setTimeout( function() {
console.log("I wait 1 second!");
}, 1000 );
复制代码
上述是匿名函数表达式,由于function()..
没有名称标识符。
函数表达式能够匿名,但函数声明不能够省略函数名。
匿名函数表达式有如下缺点:
arguments.callee
引用
解决方案:
行内函数表达式能够解决上述问题,始终给函数表达式命名是一个最佳实践。
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
console.log( "I waited 1 second!" );
}, 1000 );
复制代码
当即执行函数表达式(IIFE,Immediately Invoked Function Expression)
匿名/具名函数表达式
第一个( )将函数变成表达式,第二个( )执行了这个函数
var a = 2;
(function IIFE() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
复制代码
改进型(function(){ .. }())
用来调用的( )被移进了用来包装的( )中。
当作函数调用并传递参数进去
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
复制代码
解决undefined
标识符的默认值被错误覆盖致使的异常
将一个参数命名为undefined
,可是在对应的位置不传入任何值,这样就能够保证在代码块中undefined
标识符的值真的是undefined
。
undefined = true;
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log("Undefined is safe here!");
}
})();
复制代码
倒置代码的运行顺序,将须要运行的函数放在第二位,在IIFE执行以后当作参数传递进去
函数表达式def
定义在片断的第二部分,而后当作参数(这个参数也叫作def)被传递进IIFE函数定义的第一部分中。最后,参数def(也就是传递进去的函数)被调用,并将window传入当作global参数的值。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
复制代码
表面上看JavaScript并无块做用域的相关功能,除非更加深刻了解(with、try/catch 、let、const)。
for (var i = 0; i < 10; i++) {
console.log( i );
}
复制代码
上述代码中i
会被绑定在外部做用域(函数或全局)中。
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something( bar );
console.log( bar );
}
复制代码
上述代码中,当使用var声明变量时,它写在哪里都是同样的,由于它们最终都会属于外部做用域。
块做用域的一种形式,用with
从对象中建立出的做用域仅在**with
声明中**而非外部做用域中有效。
ES3规范中规定try/catch的catch分句会建立一个块做用域,其中声明的变量仅在catch中有效。
try {
undefined(); // 执行一个非法操做来强制制造一个异常
}
catch (err) {
console.log( err ); // 可以正常执行!
}
console.log( err ); // ReferenceError: err not found
复制代码
当同一个做用域中的两个或多个catch分句用一样的标识符名称声明错误变量时,不少静态检查工具仍是会发出警告,实际上这并非重复定义,由于全部变量都会安全地限制在块做用域内部。
ES6引入了let
关键字,能够将变量绑定到所在的任意做用域中(一般是{ .. }
内部),即let
为其声明的变量隐式地劫持了所在的块做用域。
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
复制代码
存在的问题
用let
将变量附加在一个已经存在的的块做用域上的行为是隐式的,若是习惯性的移动这些块或者将其包含在其余的块中,可能会致使代码混乱。
解决方案
为块做用域显示地建立块。显式的代码优于隐式或一些精巧但不清晰的代码。
var foo = true;
if (foo) {
{ // <-- 显式的块
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
复制代码
在if声明内部显式地建立了一个块,若是须要对其进行重构,整个块均可以被方便地移动而不会对外部if声明的位置和语义产生任何影响。
在let进行的声明不会在块做用域中进行提高
console.log( bar ); // ReferenceError
let bar = 2;
复制代码
一、垃圾收集
function process(data) {
// 在这里作点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
console.log("button clicked");
}, /*capturingPhase*/false );
复制代码
click
函数的点击回调并不须要someReallyBigData
。理论上当process(..)
执行后,在内存中占用大量空间的数据结构就能够被垃圾回收了。可是,因为click
函数造成了一个覆盖整个做用域的闭包,JS引擎极有可能依然保存着这个结构(取决于具体实现)。
二、let循环
for (let i = 0; i < 10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
复制代码
for循环头部的let不只将i绑定到了for循环的块中,事实上它将其从新绑定到了循环的每个迭代中,确保使用上一个循环迭代结束时的值从新进行赋值。
{
let j;
for (j = 0; j < 10; j++) {
let i = j; // 每一个迭代从新绑定!
console.log( i );
}
}
复制代码
ES6引用了const
,能够建立块做用域变量,但其值是固定的(常量)
var foo = true;
if(foo) {
var a = 2;
const b = 3; // 包含在if中的块做用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
复制代码
var a = 2;
会被当作两个声明,var a;
和a = 2;
,第一个声明在编译阶段进行,第二个赋值声明会被留在原地等待执行阶段。a = 2;
var a;
console.log( a ); // 2
---------------------------------------
// 实际按以下形式进行处理
var a; // 编译阶段
a = 2; // 执行阶段
console.log( a ); // 2
复制代码
console.log( a ); // undefinde
var a = 2;
---------------------------------------
// 实际按以下形式进行处理
var a; // 编译
console.log( a ); // undefinde
a = 2; // 执行
复制代码
function foo() {
var a;
console.log( a ); // undefinde
a = 2;
}
foo();
复制代码
foo(); // 不是ReferenceError,而是TypeError!
var foo = function bar() {
// ...
};
复制代码
上面这段程序中,变量标识符foo()
被提高并分配给所在做用域,所以foo()
不会致使ReferenceError。此时foo
并无赋值(若是它是一个函数声明而不是函数表达式,那么就会赋值),foo()
因为对undefined
值进行函数调用而致使非法操做,所以抛出TypeError
异常。
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
---------------------------------------
// 实际按以下形式进行处理
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
};
复制代码
foo(); // 1
var foo;
function foo() {
console.log( 1 );
};
foo = function() {
console.log( 2 );
};
---------------------------------------
// 实际按以下形式进行处理
function foo() { // 函数提高是总体提高,声明 + 赋值
console.log( 1 );
};
foo(); // 1
foo = function() {
console.log( 2 );
};
复制代码
var foo
尽管出如今function foo()...
的声明以前,但它是重复的声明,且函数声明会被提高到普通变量以前,所以被忽略foo(); // 3
function foo() {
console.log( 1 );
};
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
};
复制代码
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log( "a" ); };
}
else {
function foo() { console.log( "b" ); };
}
复制代码
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 ---- 这就是闭包的效果
复制代码
bar()
在本身定义的词法做用域之外的地方执行。
bar()
拥有覆盖foo()
内部做用域的闭包,使得该做用域可以一直存活,以供bar()
在以后任什么时候间进行引用,不会被垃圾回收器回收
bar()
持有对foo()
内部做用域的引用,这个引用就叫作闭包。// 对函数类型的值进行传递
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 这就是闭包
}
foo();
复制代码
baz
传递给bar
,当调用这个内部函数时(如今叫作fn
),它覆盖的foo()
内部做用域的闭包就造成了,由于它可以访问a。// 间接的传递函数
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz分配给全局变量
}
function bar() {
fn(); // 这就是闭包
}
foo();
bar(); // 2
复制代码
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
复制代码
setTimeout(..)
持有对一个参数的引用,这里参数叫作timer,引擎会调用这个函数,而词法做用域在这个过程当中保持完整。这就是闭包// 典型的闭包例子:IIFE
var a = 2;
(function IIFE() {
console.log( a );
})();
复制代码
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
}
//输入五次6
复制代码
i
的最终值。i
尝试方案1:使用IIFE增长更多的闭包做用域
for (var i = 1; i <= 5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
})();
}
//失败,由于IIFE做用域是空的,须要包含一点实质内容才可使用
复制代码
尝试方案2:IIFE增长变量
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})();
}
// 正常工做
复制代码
尝试方案3:改进型,将i
做为参数传递给IIFE函数
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})( i );
}
// 正常工做
复制代码
let
能够用来劫持块做用域,而且在这个块做用域中声明一个变量。for (var i = 1; i <= 5; i++) {
let j = i; // 闭包的块做用域!
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
}
// 正常工做
复制代码
for
循环头部的let
声明会有一个特殊的行为。变量在循环过程当中不止被声明一次,每次迭代都会声明。随后的每一个迭代都会使用上一个迭代结束时的值来初始化这个变量。上面这句话参照3.4.3–---2.let循环,即如下
{
let j;
for (j = 0; j < 10; j++) {
let i = j; // 每一个迭代从新绑定!
console.log( i );
}
}
复制代码
循环改进:
for (let i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log( i );
}, i * 1000 );
}
// 正常工做
复制代码
模块模式须要具有两个必要条件:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! ") );
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
// 一、必须经过调用CoolModule()来建立一个模块实例
// 二、CoolModule()返回一个对象字面量语法{ key: value, ... }表示的对象,对象中含有对内部函数而不是内部数据变量的引用。内部数据变量保持隐藏且私有的状态。
复制代码
当即调用这个函数并将返回值直接赋予给单例的模块标识符foo。
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! ") );
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
复制代码
大多数模块依赖加载器/管理器本质上是将这种模块定义封装进一个友好的API。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++ ) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps ); // 核心,为了模块的定义引用了包装函数(能够传入任何依赖),而且将返回值(模块的API),储存在一个根据名字来管理的模块列表中。
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
复制代码
使用上面的函数来定义模块:
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduct: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" );
) // Let me introduct: hippo
foo.awesome(); // LET ME INTRODUCT: HIPPO
复制代码
在经过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每一个模块均可以导入其余模块或特定的API成员,一样能够导出本身的API成员。
ES6模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)
// bar.js
function hello(who) {
return "Let me introduct: " + who;
}
export hello;
// foo.js
// 仅从“bar”模块导入hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello( hungry ).toUpperCase();
);
}
export awesome;
// baz.js
// 导入完整的“foo”和”bar“模块
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino")
); // Let me introduct: rhino
foo.awesome(); // LET ME INTRODUCT: HIPPO
复制代码
import
:将一个模块中的一个或多个API导入到当前做用域中,并分别绑定在一个变量上module
:将整个模块的API导入并绑定到一个变量上。export
:将当前模块的一个标识符(变量、函数)导出为公共APIthis
机制某种程度上很像动态做用域。// 词法做用域,关注函数在何处声明,a经过RHS引用到了全局做用域中的a
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
-----------------------------
// 动态做用域,关注函数从何处调用,当foo()没法找到a的变量引用时,会顺着调用栈在调用foo()的地方查找a
function foo() {
console.log( a ); // 3(不是2!)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
复制代码
ES3开始,JavaScript中就有了块做用域,包括with和catch分句。
// ES6环境
{
let a = 2;
console.log( a ); // 2
}
console.log( a ); // ReferenceError
复制代码
上述代码在ES6环境中能够正常工做,可是在ES6以前的环境中如何实现呢?
答案是使用catch分句,这是ES6中大部分功能迁移的首选方式。
try {
throw 2;
} catch (a) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
复制代码
// 代码转换成以下形式
{
try {
throw undefined;
} catch (a) {
a = 2;
console.log( a ); // 2
}
}
console.log( a ); // ReferenceError
复制代码
let
声明会建立一个显式的做用域并与其进行绑定,而不是隐式地劫持一个已经存在的做用域(对比前面的let
定义)。
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
复制代码
存在的问题:
let
声明不包含在ES6中,Traceur编译器也不接受这种代码
/*let*/ { let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
复制代码
{
let a = 2;
console.log( a );
}
console.log( a ); // ReferenceError
复制代码
进阶系列文章汇总以下,内有优质前端资料,以为不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!