作用域
编译
- 分词/词法分析(tokenizing/lexing)-----将代码块拆分为词法单元(token)
- 解析/语法分析(parsing)-----将词法单元转换为抽象语法树(AST)
- 代码生产-----将AST转换为可执行代码
关键角色
- 引擎:从头到尾负责整个JavaScript的编译和执行过程
- 编译器:负责词法分析以及代码生成
- 作用域:负责收集并维护所有声明的标识符组成的一系列查询,并实施严格的规则,确定当前执行代码对标识符的访问权限
编译器在对 var a = 2 进行代码生成时会进行如下处理
- 编译器询问作用域是否有一个该名称的变量存在于同一作用域中,如果是,则忽略该声明,继续进行编译,否则会在当前作用域中声明一个新的变量,命名为a
- 然后编译器会生成引擎运行时所需的代码,这些代码用于完成 a = 2 这个赋值操作,然后引擎运行时会询问作用域,在当前作用域中是否存在一个 a 变量,如果是,则使用,否则,继续向上层查找,直到全局作用域
引擎在执行时,会对变量进行查询
- RHS,查询某个变量的值
- LHS,查询某个变量的容器本身,从而可对其赋值
异常
- RHS,在所有嵌套作用域都未查询到变量则会抛出 ReferenceError
- LHS,在全局作用域查找不到时,则会在全局作用域创建该名称的变量,严格模式下则会抛出 ReferenceError
如果查询正确,但操作不合理,如调用非函数变量,则会抛出TypeError
词法作用域
作用域查找会在找到第一个匹配的标识符时停止
多层嵌套作用域中定义同名标识符,可产生遮蔽效应
作用域查找始终从运行所处的内部作用域开始,逐级向上
全局变量会自动成为全局对象(浏览器中的window)的属性,因此可间接通过全局对象属性的引用来访问(在全局变量被同名变量遮蔽时)
无论函数在哪里调用,如何被调用,词法作用域只由声明时的位置决定
词法作用域只查找一级标识符,如foo.bar.baz只会查找foo标识符
欺骗词法
- eval()接受一个字符串为参数,并运行,会对所在作用域进行修改
- with根据传递的对象创造一个全新的作用域,并将对象的属性解析到新的作用域
eval和with都会影响性能
函数作用域和块作用域
函数作用域是指属于这个函数的全部变量可以在整个函数的范围内(嵌套也能)使用和复用
let a=2;
function foo() {
let a=3;
console.log(a)
}
foo();
console.log(a);
一般声明具名函数,其函数名本身会污染所在作用域
let a=2;
(function foo() {
let a=3;
console.log(a);
})();
console.log(a);
立即执行函数名会被绑定在函数内部,即外部无法访问
undefined=true;
(function (undefined) {
let a;
if (a===undefined){
console.log("undfined is safe here!");
}
})();
该函数保证了内部undefined的值为undefined
JavaScript为函数作用域,该循环中的变量可在外部访问到
for (var i=0;i<2;i++){
var a=100;
console.log(i);
}
console.log(i);
console.log(a);
with也是块作用域,从对象中创建的作用域仅在with中有效
try/catch的catch分句也会创建一个块作用域
try {
undefined();
}
catch (e) {
console.log(e);
}
console.log(e);
let会将变量隐式的绑定在所在的块作用域,且不会提升声明,在循环中let会将迭代变量绑定到每一次迭代块作用域中
let foo=true;
if (foo){
let bar=foo*2;
console.log(bar);
}
console.log(bar);
console.log(str);
let str='he';
块作用域与垃圾回收的相关
function pro(data) {}
let obj={};
pro(obj);
let btn=document.getElementById('div');
btn.addEventListener("click",function (ev) {
console.log("button");
},false);
由于有回调函数,该作用域不会被回收,无关过程会占据大量内存,可将包装在块作用域中
function pro(data) {}
{
let obj={};
pro(obj);
}
let btn=document.getElementById('div');
btn.addEventListener("click",function (ev) {
console.log("button");
},false);
const也可用于创建块作用域变量,其值为固定
let foo=true;
if (foo){
const b=3;
console.log(b);
}
console.log(b);
提升
考虑以下代码
a=2;
var a;
console.log(a);
console.log(b);
var b;
变量和函数的声明会在代码执行前被处理,上述代码会按如下处理
var a;
a=2;
console.log(a);
var b;
console.log(b);
b=2;
只有声明本身会被提升,赋值和其他运行逻辑依旧,且每个作用域都会进行提升
foo();
function foo() {
console.log(a);
var a=2;
}
函数表达式不会被提升
foo();
var foo=function () {
console.log('haha');
};
即使具名函数表达式,名称表示符在赋值前也无法在所在作用域中使用
foo();
bar();
var foo=function bar() {
console.log('hah');
};
上述代码被提升后会被理解为以下形式
var foo;
foo();
bar();
foo=function () {
var bar=...self...;
};
函数优先,函数声明和变量声明都会被提升,但是函数会先提升
foo();
var foo;
function foo() {
console.log(1);
}
foo=function () {
console.log(2);
};
function foo() {
console.log(1);
}
foo();
foo=function () {
console.log(2);
};
后面的函数声明会覆盖前面的
foo();
function foo() {
console.log(1);
}
var foo=function () {
console.log(2);
};
foo();
function foo() {
console.log(3);
}
foo();
作用域闭包
当函数可记住并访问所在的词法作用域时,就产生了闭包,即使是函数在当前词法作用域之外执行
在所在作用域执行,bar被封闭在foo的作用域中,更偏向作用域查找
function foo() {
let a=2;
function bar() {
console.log(a);
}
bar();
}
foo();
在定义作用域外执行,通常foo在执行后会被销毁,但事实上内部作用域依然存在,bar任然在使用这个作用域,bar对该作用域的引用就叫做闭包
function foo() {
let a=2;
function bar() {
console.log(a);
}
return bar;
}
let baz=foo();
baz();
循环和闭包
for (var i=1;i<=5;i++){
setTimeout(function () {
console.log(i);
},1000*i);
}
由于i被声明在同一作用域,循环结束后,当前作用域中只有i=6,IIFE(立即执行函数)可用来创建作用域,但i并没有绑定在作用域中
for (var i=1;i<=5;i++){
(function () {
setTimeout(function () {
console.log(i);
},1000*i);
})();
}
间接存储i的值
for (var i=1;i<=5;i++){
(function () {
var j=i;
setTimeout(function () {
console.log(j);
},1000*j);
})();
}
进行改进
for (var i=1;i<=5;i++){
(function (j) {
setTimeout(function () {
console.log(j);
},1000*j);
})(i);
}
使用let来劫持块作用域,在块中声明变量
for (var i = 1; i <= 5; i++) {
let j=i;
setTimeout(function () {
console.log(j);
}, 1000 * j);
}
在循环头使用let声明时,每次迭代都会声明,随后每次跌打都会使用上一次迭代结束时的值来初始化这个变量
for (let i=1;i<=5;i++){
setTimeout(function () {
console.log(i);
},1000*i);
}
模块
模块模式
- 有外部封装函数且被调用(每次调用都会创建一个模块实例)
- 函数内部要返回一个内部函数,才能在私有作用域中形成闭包,并且可访问或修改私有状态
function CoolModule() {
let something='cool';
let another=[1,2,3];
function doSomething() {
console.log(something);
}
function doAnther() {
console.log(another);
}
return {
doSomething:doSomething,
doAnther:doAnther
};
}
foo.doSomething();
foo.doAnther();
当只需要一个实例时,可用单列模式
let foo=(function CoolModule() {
let something='cool';
let another=[1,2,3];
function doSomething() {
console.log(something);
}
function doAnther() {
console.log(another);
}
return {
doSomething:doSomething,
doAnther:doAnther
};
})();
foo.doSomething();
foo.doAnther();
模块也是函数,可接受参数
function CoolModule(id) {
function identify() {
console.log(id);
}
return {
identify:identify
};
}
let foo1=CoolModule(1);
let foo2=CoolModule(2);
foo1.identify();
foo2.identify();
命名将要作为公共API返回的对象
let foo=(function CoolModule(id) {
function change() {
publicAPI.identify=identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
let publicAPI={
change:change,
identify:identify1
};
return publicAPI;
})('foo module');
foo.identify();
foo.change();
foo.identify();
现代模块机制
let MyModules=(function Manager() {
let modules={};
function define(name,deps,impl) {
for (let i=0;i<deps.length;i++){
deps[i]=modules[deps[i]];
}
modules[name]=impl.apply(impl,deps);
}
function get(name) {
return modules[name];
}
return {
define:define,
get:get
};
})();
MyModules.define('bar',[],function () {
function hello(who) {
return "let me introduce: "+who;
}
return {
hello:hello
};
});
MyModules.define('foo',['bar'],function (bar) {
let hungry='hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome:awesome
};
});
let bar=MyModules.get('bar');
let foo=MyModules.get('foo');
console.log(bar.hello('hippo'));
foo.awesome();
未来的模块机制
bar.js
function hello(who){
return "Let me introduce: "+who;
}
export hello;
foo.js
import hello from "bar";
var hungry="hippo";
function awesome(){
console.log(hello(hungry).toUpperCase());
}
export awesome;
baz.js
module foo from "foo";
module bar from "bar";
console.log(bar.hello("rhino"));
foo.awesome();
动态作用域
动态作用域并不关心函数和作用域时如何声明以及在何处声明,只关心从何处调用,也就是说作用域链是基于调用栈,而不是代码的作用域嵌套
function foo() {
console.log(a);
}
function bar() {
let a=3;
foo();
}
let a=2;
bar();
词法作用域让foo()中的a通过RHS引用了全局作用域的a,所以输出2
而动态作用域foo()无法找到a的变量引用时,会顺着调用栈,在调用foo的地方查找a,就会i输出3
箭头函数(=>)
写法如下
let foo=a=>{
console.log(a);
};
foo(2);
但箭头函数不仅仅是function的简写,更重要的是解决丢失this的绑定
let obj={
id:'awesome',
cool:function () {
console.log(this.id);
}
};
let id="not cool";
obj.cool();
setTimeout(obj.cool,100);
cool函数丢失了同this的绑定,利用词法作用域
let obj={
count:0,
cool:function () {
let self = this;
if (self.count<1){
setTimeout(function () {
self.count++;
console.log("awesome?");
},100);
}
}
};
obj.cool();
利用箭头函数,在涉及this绑定时,箭头函数会放弃普通this绑定规则,而是用当前的词法作用域覆盖this本来的值
let obj={
count:0,
cool:function () {
if (this.count<1){
setTimeout(()=>{
this.count++;
console.log("awesome?");
},100);
}
}
};
obj.cool();
bind方式
let obj={
count:0,
cool:function () {
if (this.count<1){
setTimeout(function () {
this.count++;
console.log("more awesome");
}.bind(this),100);
}
}
};
obj.cool();