前言
这期分享javascrit的预编译的内容,这里面涉及到很多知识点,概念。结合自己的理解,再加上实例,做一次完整总结。主要内容有:整个编译流程介绍。在这个之上还会涉及到,作用域,闭包,以及this等知识点。
脚本预编译
编译流程:
—> 脚本:
创建全局对象GO(window) ( 上下文)
加载脚本文件
预编译:
* 找出所有的变量声明,按照变量名加入全局对象,如果已经存在,忽略该变量声明。
* 找出所有函数声明,按照函数名加入全局对象,若果已经存在同名变量和函数,替换。(如果有变量声明和函数声明的,不管两者出现的先后位置如何,函数都会优先替换与他同名变量。)
* 非声明不予理睬。(预编译阶段除了变量,函数声明,其他语句待处理)
解释-执行
下面用实例拆解每一个编译步骤:
let a = i;
function b(xx) {
let xx = 'abc';
funciton xx(yy){
let xx = 1;
}
};
let c = function() {};
function a () {};
aa = 15;
b(100);
创建全局对象GO,加载全局变量
window.a -> undefined
window.b -> function() {}
window.c -> undefined
window.a -> function(){}
//预编译完成 开始执行
window.a -> 1,
window.b -> function(){}
window.c -> function(){}
window.a - 1 //
window.aa -> 15;
这里分析编译时变量函数声明,加载到全局过程
函数的预编译-函数调用
函数调用:
创建活动对象AO上下文(object)
预编译:
scope-chain
初始化arguments
初始化形参,类数组中的值传给形参。
加入全局变量
加入函数声明。
this 初始化。
函数执行。
结合上面列子继续分析函数预编译过程
let a = i;
function b(xx) {
let xx = 'abc';
funciton xx(yy){
let xx = 1;
}
};
let c = function() {};
function a () {};
aa = 15;
b(100);
//创建全局对象GO,加载全局变量
window.a -> undefined
window.b -> function() {}
window.c -> undefined
window.a -> function(){}
//预编译完成 开始执行
window.a -> 1,
window.b -> function(){}
window.c -> function(){}
window.a - 1 // 这里复制声明,覆盖了函数声明,因此结果是 ---> 1
window.aa -> 15;
//调用函数,调入函数体内,函数预编译。
生成AO,
AO-object.xx: 'abc',
AO-object.xx:function(){}
//由于作用域的提升规则,实际编译结果是:
AO-object-xx:'abc',
执行结果
.....
作用域(scope)
js中的作用域是基于函数函数级别的,没有块级别的作用域概念。(es5是这样),换句话说,就是进入函数或者退出函数,函数的作用域都会发生变化。
它是用于查询变量的一套规则。程序中函数声明位置,就出现了作用域。作用域不是单独的存在的,他像一条链,名为作用域链,存在每一个函数声明中。我们在任何地方引用变量时,就会去遍历作用域链查找变量。
变量提升
变量的声明位置,与作用域紧密联系. 变量(函数)在程序中出现的位置不同,那么执行结果也会不同。其中原因就是变量提升。
看一段程序。
console.log(a) //undefined
var a = 2;
键入控制台执行,可以看到结果为 undefined,这里并不是报错,refrenceERROR. a is not defined, 而是 a = undefined。为什么会这样,我们企图在调用一个未定义变量,难道不应该报错吗?为什么会这样?
这里就是变量提升了,准确的说在预编译结束后,未初始化变量就引用变量。
函数优先
首先说,函数和变量都会被提升。当程序中函数和变量重名时,函数优先提升。具体是个什么效果呢,实例来。
foo(); //1
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
};
//这里结果是1而不是 2 ! 同样是提升。
//这段程序重写为
function foo() {
console.log(1);
}
foo();
var foo = function() {
console.log(2);
};
重要说明:程序中避免出现变量重名,否则程序会更加难以维护。
作用域链(scope-chain)
什么是作用域链,简单来说就是各函数作用域(AO+ GO)像串珠子一样,串连在一起,形成的一条链。它的作用是,在执行环境内,通过作用域链查找变量和函数。
函数作用域链的生成
- 每个函数在定义(函数声明或者函数表达式)时,会拷贝其父亲函数的作用域链。
- 函数在调用时,生成的AO然后将AO压入作用域链的栈定。
实例拆解分析,如下是模拟的一个具体的从脚本编译到执行的过程。
let sToken = 'g';
function foo() {
let sToken_a = 'a';
function fn(){
let sToken_b = 'b';
}
fn();
}
foo();
//栈内存
// ST001:GEC-SC:HP001 脚本执行,生成作用域链(栈),作用域链(栈)是个对象,存在堆内。
// ST002:
// ST003:
// ST004:
// ST005:
// ST006:
// //堆内存
// HP001:GEC-SC:[ HP002-(GO) ](1) // 全局作用域链的第一次引用,他是一个数组,value指向GO的引用
// HP002:Go:{sToken:'g', this:window, foo:function(){ HP002-(GO) //GO作用域链拷贝 }} (3)
// HP003:fOO-ESC-SC: {HP002-(GO):window,HP004(foo-AO)}(1) //函数foo scope-chain压栈
// HP004:foo-AO:{this:window,sToken_a:'a',fb:function(){ [HP002-(AO),HP004(fa-AO)] }} (2)
// HP005:fN-AO:{this:window,sToken_b:'b'} (1)
// HP006:fNEC-SC:{HP002(GO),HPOO4(FOO-AO),HP005-AO}(1) //压栈
改变作用域
js中使用with ,能骗过编译过程的词法分析,改变作用域.用处不多,了解即可。
let object = {
name: 'shanshan'
}
with(object){
console.log(name);
}
//这里的属性值name,实际在with形成的作用域中引用。
// 他可以改变对象的原有词法作用域,形成一个完全隔离的词法作用域,然后压入作用域栈链的栈顶。
附:
执行环境:
函数调用会生成执行上下文,执行上下文对应着一个scope-chain。而scope-chain其本质来说是一个引用类型的栈对象。类似数组的一个结构。里面存有当执行环 境的上下文。
作用域链形成阶段会有一个压栈过程,他是存在于作用域链的一个栈(虚拟的栈,有js引擎创建并访问)。
js引擎遇到的每一个函数执行,都会形成单独的作用域,作用域链(scope chain).scope chain 独立每一个函数的运行期间,
意思是每一层函数调用,都会存在各自的scope chain栈。函数在执行时,会生成执行上下文,同事还会拷贝当前父辈的scope chain.
形成自己的scope chain,用于函数变量的查询。
执行环境分为:
- 全局执行环境
GO
见到脚本时创建EC
网页关闭时销毁。(所以说除非网页关闭,否则全局对象一直存在) - 函数执行环境
AO
从函数调用开始创建
导函数退出时销毁(这里体现了立即执行函数的好处)
执行环境可以理解为当前的一个作用域,AO或者GO
作用域链应用
- 效率:
尽量减少使用考经上层的变量,提高查找效率。 - 重名:
减少不同层次的变量重名
避免函数名,变量重名。
函数执行完时,AO一定会销毁吗?接下来才是主角登场。。。。
闭包
函数的AO通过scope-chain 相互连接起来,使得父函数体内的变量可以保存在子函数的AO中.这一效果称之为闭包。
首先见识一下闭包:
function outer(){
let scope = 'outer';
function inner(){
return scope;
}
return inner;
}
let fn = outer(); //outer
console.log(fn( )); //这里我们意外拿到了已经’销毁‘函数的变量 scope, oh! my god
闭包都有哪些神奇之处呢,一起看看
- 实现共有变量的存储
- 缓存存储结构
- 封装。实现属性私有化
- 模块化开发,防止全局变量污染。
//实现共有变量,比如累加器
function add(){
let count = 0;
function addAction(){
count++;
console.log(count);
return count;
}
return addAction;
}
let myAdd = add();
myAdd();
myAdd();
myAdd();
//缓存存储结构,把父函数多个返回,用数组返回。
function add(){
let count = 0;
function addAction(){
count++;
console.log(count);
return count;
}
function clearAction(){
count = 0;
console.log(count);
return count;
}
return [addAction,clearAction];
}
let myAdd = add();
myAdd[0]();
myAdd[0]();
myAdd[1]();
myAdd[0]();
myAdd[0]();
//重写
function add(){
let count = 0;
let adder = {
addAction:function (){
count++;
console.log(count);
return count;
},
clearAction:function (){
count = 0;
console.log(count);
return count;
}
}
return adder;
}
let myAdd = add();
myAdd.addAction();
myAdd.addAction();
myAdd.addAction();
myAdd.addAction();
myAdd.addAction();
myAdd.addAction();
myAdd.clearAction();
myAdd.addAction();
myAdd.addAction();
myAdd.addAction();
以上便是闭包的实际应用。
还有一个案例个人觉得比较经典的问题,可谓始于闭包,终于闭包。
// function outer(){
// let result = new Array();
// for(var i = 0; i < 2; i++){
// result[i] = function(){
// return i; //i = 2;
// };
// }
// return result;
// }
// let fn1 = outer();
// console.log(fn1[0]());
// console.log(fn1[1]());
// 2 2
//解决方案
// function outer(){
// let result = new Array();
// for(var i = 0; i < 2; i++){
// result[i] = (function(x){
// return function foo(){
// return x; //i = 2;
// }
// })(i);
// }
// return result;
// }
// let fn1 = outer();
// console.log(fn1[0]());
// console.log(fn1[1]());
//2.let
// function outer(){
// let result = new Array();
// for(let i = 0; i < 2; i++){
// result[i] = function(){
// return i; //i = 2;
// };
// }
// return result;
// }
// let fn1 = outer();
// console.log(fn1[0]());
// console.log(fn1[1]());
this用法总结
this是js当中,很不好掌握的一个知识点,由于它的多边性,使的使用它的人很不好把握去向。
不要方,如下几点,轻松掌握this。
1.脚本中,this初始化为window
2.普通函数中,this初始化window
3.在object中调用的函数,this被指定为第object,谁调用,指向谁。
//注意赋值:会隐式绑定
4.call/apply中,this可以被指定,被指定为第一参数。
5.在new构造函数中,this被指向正在创建对象。