希望是附丽于存在的,有存在,便有希望,有希望,便是光明。-- 鲁迅
编译原理
下述分析过程可在 esprima 网址查看
尽管将javascript归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。与传统编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
分词/词法分析
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块统称为词法单元。例如:var a = 2;这段程序会被分解成 var、a 、=、2;空格是否被当做词法单元取决于空格在这门语言中是否有意义。
解析/语法分析
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套的所组成的代表了程序语法结构的树。这个树被统称为抽象语法树(AST)。
代码生成
将AST转换成可执行代码的过程称为代码生成。
理解作用域--词法作用域
简单的说词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。(大部分情况下,也会有特殊情况)
var a = 2 浏览器经过经过上述编译过程之后会进行如下处理:
- 遇到 var a , 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明继续进行编译;否则他会要求作用域在当前作用域的集合中声明一个新变量并命名为a 。
- 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎会先询问作用域,在当前的作用域中是否存在一个叫a 的变量。如果是 引擎会使用这个变量,如果否,引擎会继续查找该变量(作用域嵌套)。
如果最终找到了a变量则将2赋值给它,否则抛出一个异常。
LHS、RHS
当变量出现在赋值操作左侧时进行LHS查询,出现在右侧时进行RHS查询。左查询找不到变量时会声明一个,右查询如果找不到会报错。
示例:
function foo(a){
console.log(a + b);
}
foo(); // ReferrenceError
解析: 当变量出现在赋值操作左边时会进行LHS查询,出现在右侧时会出现RHS查询,第一次对b进行RHS查询是无法找到该变量的,在其相关作用域中也是查询不到相关变量的,这时RHS查询就会抛出ReferrenceError异常错误
function foo2(a){
b = a + 1;
console.log(b);
}
foo2() NAN
解析: 当引擎在执行LHS时,如果再顶级作用域中也没有找到变量,“全局作用域”中就会创建一个具有该名称的变量,前提是在非严格模式下
var a = 4;
function foo3 () {
console.log(a);
var a = 5;
}
foo3(); // undefined
解析:函数foo3内部的代码先编译,会对a变量LHS,所以会在当前作用域中存在 var a 然后才是执行和赋值操作。
函数作用域
属于这个函数的全部变量都可以再整个函数的范围内使用及复用(事实上在嵌套的作用域中也能使用)。
var a = 100;
function test(){
console.log(a);
}
function testFun(){
var a = 200;
test();
}
testFun(); // 100
解析:作用域为词法作用域,即:作用域和代码声明位置有关和调用执行位置无关。
在编译时,
全局作用域中会存有的对象
a
test
testFun
testFun作用域中存的
a
test
- 无
在运行时,
test() 我这里需要变量a 啊,但是在test的作用域中并不存在,那么就需要去全局作用域中寻找 a,报告老大找到了,输出全局作用域中的a,输出100
块作用域
- 在块作用域中使用var声明变量 则 变量还是会污染 函数作用域或者全局作用域
- let / const 关键字可以将变量绑定到所在的任意作用域中(暂时性死区), 并且用let 或者const声明的变量不会提升。
var a = 4;
function foo3 () {
console.log(a);
const a = 5;
}
foo3(); // RefrenceError
解析:let/const 声明的变量不会被提升,所以会报错。
欺骗词法
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'
会使代码被当做本来就在那里一样来处理。所以不会找到外面的b,只会找到函数内部的b。
With
function foo (obj) {
with (obj) {
a = 2
}
}
var o1 = {
a :1
}
var o2 = {
b: 2
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 2 不好 a 被泄漏到全局了
解析: o1 中存在a 属性,所以将o1中的a属性重新赋值了,但是 o2中不存在a属性,所以经过LHS会在全局作用域生命一个a变量并进行赋值(非严格模式下)。
通常不建议使用eval 和 with
闭包
什么是闭包
当函数可以记住并访问所在的词法作用域时就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包的效果
循环和闭包
for( var i = 0; i < 5 ; i++ ) {
setTimeout(()=> {
console.log(i);
}, i * 1000)
} // 每隔1s 输出 一个 5
解析: setTimeout 执行函数时 for循环已经执行完毕,词法作用域中的i = 5, 会输出的都是5。
如果将上述代码改写成输出 0 1 2 3 4 呢?
- 用立即执行匿名函数创建的函数作用域(IIFE)
for( var i = 0; i < 5 ; i++ ) {
setTimeout(((i)=> ()=> {
console.log(i);
})(i), i * 1000)
}
for( var i = 0; i < 5 ; i++ ) {
((i)=>{
setTimeout(()=> {
console.log(i);
}, i * 1000)
})(i)
}
- let
for( let i = 0; i < 5 ; i++ ) {
setTimeout(()=> {
console.log(i);
}, i * 1000)
}
- setTimeout 第3参数
for( var i = 0; i < 5 ; i++ ) {
setTimeout((i)=> {
console.log(i);
}, i * 1000, i)
}
闭包的作用
- 模块
var foo = (function () {
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变量能访问和修改something、another变量,防止外部修改
2.函数柯里化
柯里化含义: 是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且 返回结果的新函数的技术。
function curry(fn, len) {
const length = len || fn.length;
let args = [];
function sub_curry() {
if (arguments.length + args.length < length) {
args = [...args, ...arguments];
return sub_curry;
} else {
const params = [...args,...arguments];
args = [];
return fn.apply(this, params);
}
}
return sub_curry;
}
const test = curry(function(a, b, c){
console.log(a,b,c);
});
test(1)(2)(3); // 1,2,3
test(1,2)(3); // 1,2,3
test(1,2,3);// 1,2,3
- 防抖/节流
var debounce = (function(fn,delay){
var time = null;
return function(){
if(time){
clearTimeout(time)
}
var _this = this;
time = setTimeout(function(){
fn.apply(_this);
},delay);
}
})();
// 节流
var throttle = (function(fn, delay){
var preTime = +new Date;
return function(){
var nowTime = +new Date();
var _this = this;
if(nowTime - preTime > delay){
fn.call(_this);
preTime = nowTime;
}
}
})();