作用域是什么
LHS与RHS
LHS 和 RHS 的含义是赋值操作的左侧或右侧
并不一定意味着就是= 赋值操作符的左侧或右侧
。赋值操作还有其他几种形式,因此在概念上最好将其理解为赋值操作(LHS)
以及获取变量值(RHS)
。
function foo(a) {// a的LHS引用,把2赋给a
console.log( a ); // console.log的RHS引用,内置对象的调用;a的RHS应用,a的调用;log函数中第一个参数a的赋值,LHS引用
}
foo( 2 ); // foo的RHS引用,foo的调用1
function foo(a){
var b=a;//b赋值,LHS;a调用,RHS
return a+b;//a,b调用,RHS两次
}
var c=foo(2);//c赋值,LHS;foo调用,RHS;参数a=2,LHS
作用域
LHS 和 RHS 引用都会在当前作用域进行查找,如果还是没有找到就继续向上,以此类推。
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
对 b 进行的 RHS 引用无法在函数 foo 内部完成,但可以在上一级作用域(在这个例子中就是全局作用域)中完成。
异常
区分 LHS 和 RHS 的意义
- 在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。
- 如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常。
- 当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是运行在非“严格模式”下。严格模式下,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常。
- 如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。
- ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
第一次对 b 进行 RHS 查询时是无法找到该变量的。也就是说,这是一个“未声明”的变量,因为在任何相关的作用域中都无法找到它。
词法作用域
词法阶段
定义在词法阶段的作用域,决定于代码的位置,大部分情况下保持作用域不变
function foo(a){
var b=a*2;
function bar(c){
console.log(a,b,c);
}
bar(b*3);
}
foo(2);//2,4,12
- 最外层包含全局作用域,只有一个标识符:foo
- 包含foo所创建的作用域,有三个标识符:a、bar、b
- 包含bar创建的作用域,只有一个标识符:c
查找
- 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇到第一个匹配的标识符为准
- 全局变量会自动成为全局对象的属性,因此可以通过全局对象属性的引用来直接访问,即window.a,但非全局的变量屏蔽了之后,怎么也访问不到
欺骗词法
运行时来修改词法作用域,通常会导致性能下降
eval
function foo(str,a){
eval(str); //欺骗,屏蔽了外部的b
console.log(a,b);
}
var b=2;
foo("var b=3;",1);//1,3
严格模式下,eval有着自己的词法作用域,无法修改所在域。
function foo(str){
"use strict";
eval(str);
console.log(a);//Uncaught ReferenceError: a is not defined
}
foo("var a=2;");
其它类似的还有setTimeout()和setInterval()第一个参数可以是字符串,new Function()最后一个参数可以是字符串,比eval安全一些。
with
with块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会限制在这个块的作用域中,而是在with所处的作用域中。
当o1传递给with时,with所在的作用域是o1,而这个作用域正好有一个o1.a。但当o2作为作用域时,其中并没有a的标识符,因此进行LHS查询,此时o2,foo(),全局作用域中都没有找到标识符a,因此当a=2自动创建了一个全局变量。
function foo(obj){
with(obj){
a=2;
}
// var a=3; 此语句会覆盖obj.a的赋值,使得a在foo的作用域下,故此时从全局作用域访问不到该变量。
}
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被泄露到全局作用域上
在严格模式下,with被完全禁止,在保留核心功能的前提下,间接或非安全的使用eval也被禁止。
性能
js引擎会在编译阶段进行多项优化,如果发现了eval或with,它只能简单假设关于标识符位置的判断都是无效的,最悲观的情况是即使对eval或with做了优化,所有的优化可能是无意义的,因此代码中大量使用eval或with,运行起来一定会变的非常慢。
函数作用域与块作用域
函数中的作用域
- 无论标识符的声明出现在作用域何处,这个标识符所代表的变量或函数都附属于所处的作用域。
- 函数的作用域是指,属于这个函数的全部变量都可以在整个函数范围内使用以及复用。
隐藏内部实现
用作用域来隐藏变量,遵守最小暴露原则
function doSometing(a){
b=a+doSomethingElse(a*2);
console.log(b*3);
}
function doSomethingElse(a){
return a-1;
}
var b;
doSometing(2);
更理想的方式是将b和doSometing私有化,防止有意或无意的以非预期的方式使用
function doSometing(a){
function doSomethingElse(a){
return a-1;
}
var b;
b=a+doSomethingElse(a*2);
console.log(b*3);
}
doSometing(2);
规避冲突
避免同名标识符之间的冲突,防止变量的值被意外覆盖
function foo(){
function bar(a){
i=3;//修改for循环所属作用域中的i,前面加上var可以遮蔽变量
console.log(a+i);
}
for(var i=0;i<10;i++){
bar(i*2);
}
}
foo();//无限循环
全局命名空间
var MyReallyCoolLibrary={
awesome:"stuff",
doSomething:function(){
//...
},
doAnotherThing:function(){
//...
}
};
模块管理
利用模块管理器,保持私有、无冲突的作用域
函数作用域
var a=2;
function foo(){
var a=3;
console.log(a);
}
foo();
console.log(a);
缺点
- foo这个名称本身污染了所在作用域
- 必须显式调用这个函数才能运行。
var a=2;
(function foo(){
var a=3;
console.log(a);//3
})();
console.log(a);//2
优点
- (function…开头确保是函数表达式,不是函数声明;
- 第一个片段foo被绑定在所在作用域中,可以直接通过foo()来调用;第二个foo被绑定在函数表达式自身的函数中而不是所在域中。
匿名和具名
函数表达式的缺点:
- 匿名函数在栈追踪中不会显示有意义的函数名,使得调试很困难
- 如果没有函数名,但函数引用自身只能使用已经过期的arguments.callee引用,比如在递归中。另一个函数需要自身的例子,是在事件触发后事件监听器需要解绑自身
- 匿名函数省略了对于代码可读性很重要的函数名
始终给函数表达式命名是一个最佳实践
立即执行函数表达式
var a=2;
(function(){
var a=3;
console.log(a);//3
})();
console.log(a);//2
- 第一个()将函数变成表达式,第二个()执行了这个函数,称为IIFE(立即执行函数表达式)
- 还有一种改进形式:(function(){…}()),功能上是一致的
var a=2;
(function IIFE(global){
var a=3;
console.log(a);//3
console.log(global.a);//2
})(window);
console.log(a);//2
可以将window对象引用传递进去,使得内部可以访问重名的变量。
undefined=true;//给其他代码挖了一个大坑,会覆盖undefined默认值,解决方法见下
(function IIFE(undefined){
var a;
if(a===undefined){
console.log("Undefined is safe here");
}
})();
上述代码将一个参数命名为undefined,但是在对应的位置不传入任何值,保证undefined的标识符值就是undefined
var a=2;
(function IIFE(def){
def(window);
})(function def(global){
var a=3;
console.log(a);//3
console.log(global.a);//2
})
倒置代码执行顺序,将需要运行的函数放在第二位,在UMD中被广泛使用
块作用域
for循环和if语句都不会产生块作用域,表面上看是没有块作用域的
with
with生成块作用域,仅在with声明中有效。
try/catch
ES3中规定try/catch的catch分句会创建一个块作用域
try{
undefined();//执行一个非法操作来强制制造异常
}
catch(err){
console.log(err);//正常执行
}
console.log(err);// Uncaught ReferenceError: err is not defined
let
let会创建一个绑定的块,同时let声明不会在块作用域中进行提升
垃圾收集
function process(data){
//...
}
var someRealllyBigData={/*...*/};
process(someRealllyBigData);
var btn=document.getElementById("my_button");
btn.addEventListener("click",function click(evt){
console.log("button clicked");
});
click函数的点击回调不需要someReallyBigData变量,意味着process执行完后someRealllyBigData会被回收。但由于click函数形成了一个覆盖整个作用域的闭包,js引擎极有可能保存这个数据结构。
块作用域可以解决这个问题,可以让引擎清除的知道没有必要保存someRealllyBigData了
function process(data){
//...
}
{
let someRealllyBigData={/*...*/};
process(someRealllyBigData);
}
var btn=document.getElementById("my_button");
btn.addEventListener("click",function click(evt){
console.log("button clicked");
});
let循环
for(let i=0;i<10;i++){
console.log(i);
}
console.log(i);//ReferenceError
for循环中的let不仅绑定到for循环的块中,事实上它将其绑定到循环的每个迭代中。
{
let j;
for(let j=0;j<10;j++){
let i=j;//每次迭代重新绑定
console.log(i);
}
}
相对应的如果考虑用let来替代var则需要在代码重构中付出额外的精力
var foo=true,baz=10;
if(foo){
var bar=3;
if(baz>bar){
console.log(baz);
}
//...
}
重构后:
var foo=true,baz=10;
if(foo){
var bar=3;
//...
}
if(baz>bar){
console.log(baz);
}
重构成let变量时:
var foo=true,baz=10;
if(foo){
let bar=3;
if(baz>bar){
console.log(baz);
}
//...
}
const
ES6引入了const,同样可以创建块作用域变量,但值是固定的。
var foo=truel;
if(foo){
var a=2;
const b=3;
a=3;//正常
b=4;//错误
}
console.log(a);//3
console.log(b);//ReferenceError
提升
变量和函数在内的所有声明都会在任何代码被执行前首先被处理,这个过程叫做提升。
a=2;
var a;
console.log(a);//2
编译成:
var a;
a=2;
console.log(a);
console.log(a);//undefined
var a=2;
编译成:
var a;
console.log(a);
a=2;
foo();
function foo(){
console.log(a); //undefined
var a=2;
}
编译成:
function foo(){
var a;
console.log(a);
a=2;
}
foo();
函数表达式不会提升
foo();//TypeError
var foo=function bar(){
//...
};
编译为:
var foo;
foo();//TypeError
foo=function bar(){
//...
};
即使是具名的函数表达式,名称标识符在赋值之前也无法使用
foo();//TypeError
bar();//ReferenceError
var foo=function bar(){
//...
}
编译成:
var foo;
foo();
bar();
foo=function(){
var bar = ...self...
//...
}
函数优先
函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量,冲突时重复的声明会被忽略
var foo尽管出现在function foo()…声明之前,但它是重复的声明(被忽略),因为函数声明会被提升到普通变量之前
foo();//1
var foo;
function foo(){
console.log(1);
}
foo=function(){
console.log(2);
}
编译成:
function foo(){
console.log(1);
}
foo();
foo=function(){
console.log(2);
}
尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的
foo();//3
function foo(){
console.log(1);
}
var foo=function(){
console.log(2);
}
function foo(){
console.log(3);
}
编译成:
function foo(){
console.log(3);
}
foo();//3
foo=function(){
console.log(2);
}
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,注意这个行为不可靠,在未来的版本中会发生改变,应该尽可能的避免在块内部声明函数
foo();//TypeError,按照定义应该是b,此处有疑问
var a=true;
if(a){
function foo(){console.log("a");}
}else{
function foo(){console.log("b");}
}
编译成:
function foo () { console.log("b"); }
foo();
作用域闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
function foo(){
var a=2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz=foo();
baz();//2
- 函数bar的词法作用域能够访问foo()的内部作用域,然后bar()本身当做一个值类型传递
- bar()涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,不会被垃圾回收器回收,以供bar()在之后任何时间进行引用,bar一直持有对该作用域引用,这个引用就叫做闭包
function wait(message){
setTimeout(function timer(){
//timer函数一直持有对wait作用域的闭包
console.log(message);
},1000);
}
wait("Hello,closure!");
var a=2;
(function IIFE(){
console.log(a);
})();
IIFE不是典型的闭包,因为函数并不是在它本身的词法作用域以外执行的。它是在定义时所在的作用域执行,也就是全局作用域也持有a,a是通过普通的词法作用域而非闭包被发现的。
循环和闭包
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);//五个6
},i*1000);
}
- 这个循环的终止条件i不再<=5,条件首次成立时i的值是6
-
延迟函数的回调会在循环结束时才执行,同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个)…
- 主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
- 主线程遇到异步任务,指给对应的异步进程进行处理(WEB API);
- 异步进程处理完毕(Ajax返回、DOM事件处罚、Timer等),将相应的异步任务推入任务队列;
- 主线程查询任务队列,执行microtask queue(promise,MutationObserver),将其按序执行,全部执行完毕;
- 主线程查询任务队列,执行macrotask queue(onclick,setTimeout,Ajax),取队首任务执行,执行完毕;
- 重复step4、step5。
试图输出1,2,3,4,5
for(var i=1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(i);//五个6
},i*1000);
})();
}
此时闭包函数的作用域为空,i向上进行RHS引用,直至最外层,得到i=6,即i未记住所在的词法作用域时,不能形成闭包
for(var i=1;i<=5;i++){
(function(){
var j=i;
setTimeout(function timer(){
console.log(j);//1,2,3,4,5
},j*1000);
})();
}
每次迭代都会保存i的值,i已记住所在的词法作用域,形成闭包
改进
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);//1,2,3,4,5
},j*1000);
})(i);
}
重返块作用域
可以考虑将作用域转换为块作用域
for(var i=1;i<=5;i++){
let j=i;//闭包的块作用域
setTimeout(function timer(){
console.log(j);//1,2,3,4,5
},j*1000);
}
改进
for(let i=1;i<=5;i++){
setTimeout(function timer(){//块作用域与闭包运用
console.log(i);//1,2,3,4,5
},i*1000);
}
模块
function foo(){
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 = foo();
foo.doSomething();//cool
foo.doAnother();//1,2,3
这个模式就被称为模块,模块具备的必要条件:
- 必须有外部的封闭函数,该函数至少被调用一次
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并还可以访问或者修改私有的状态
可以对模块进行改进,只需要一个实例
var foo=(function foo(){
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 foo=(function Module(id){
function change(){
publicAPI.identity=identity2;
}
function identity1(){
console.log(id);
}
function identity2(){
console.log(id.toUpperCase());
}
var publicAPI={
change:change,
identity:identity1
}
return publicAPI;
})("foo fff");
foo.identity();
foo.change();
foo.identity();
现代的模块机制
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);
}
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){
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"));
foo.awesome();
未来的模块机制
es6的import和export类似于以上机制