JS简记-作用域1
var a = 1; console.log(a);
第一行代码var a=1,会被引擎看成两句话:var a和a=1。
首先,会由编译器询问当前作用域内(这里就是全局作用域)是否存在已经声明的变量a,如果存在则忽略var a,否则在当前作用域内声明变量a(这时,作用域内就有了变量a。测试时可以发现,即便两行代码倒置顺序,并不会抛ReferenceError,而是打印undefined,说明引擎首先会提前通过编译器执行代码中所有的声明操作)。
其次,编译器声明变量a(即在作用域内生成了变量a)后,就会编译a=1语句,编译后的语句由执行引擎执行。
最后,在执行引擎执行时,也就是在执行被编译后的a=1时,首先会进行LHS(左查找,目的是找到变量所在的容器,顺着作用域链查找,如果最终在全局作用域中仍未找到,则会默认在全局作用域内声明该变量,但如果是“use strict”,则与RHS未找到一样,抛ReferenceError),然后在赋值1。
var o = {}; o.b = 1; console.log(o.b);
引擎在执行o.b=1时(假设已被编译器编译),首先会LHS查找变量o,然后使用对象访问规则访问o的b属性,这里就是对b赋值1。
eval和with
js中变量的作用域通常情况下,在书写代码时就已经确定了,但也有在运行时动态修改作用域的语法。
eval函数可以传入一个字符串,该字符串在运行时被解析为相应语句来执行。
function foo(str){ eval(str);//改变a的作用域 console.log(a);//2 } var a = 1; foo("var a = 2;");
with可以将一个对象视作为一个作用域,在with块内,可以直接引用对象中的属性,而无需再强调对象本身。在with块中仍然会使用LHS和RHS查找原则,由于o2没有a属性,所以with(o2)块中使用LHS对a赋值时,最终赋在了全局作用域上。
var o1 = { a: 1 }; var o2 = {}; with(o1){ a = 2; } with(o2){ a = 2; } console.log(o1.a);//2 console.log(o2.a);//undefined
上面我们见到了两种作用域:函数和with块。
我们通常会将一些执行逻辑放在函数内,但在js中函数还有一个重要的用途,那就是封装变量与函数,将变量与函数放在一个函数内可以很好将其私有化,可以避免将所有变量和函数暴露在全局作用域中。
我们可以这样做:
function doSomething(a) { b = a + doSomethingElse( a * 2 ); console.log( b * 3 ); } function doSomethingElse(a) { return a - 1; } var b; doSomething( 2 ); // 15
但更好的做法是:
function doSomething(a) { function doSomethingElse(a) { return a - 1; } var b; b = a + doSomethingElse( a * 2 ); console.log( b * 3 ); } doSomething( 2 ); // 15
由于将函数和变量封装起来,使得外部更加“干净”,但这样写需要先声明函数,再进行函数调用,很罗嗦,好在js提供了相应的解决策略。
function foo(a) { //... } foo(a);
相当于:
(function foo(a) { //... })(a); //or (function foo(a) { //... }(a));
这种模式叫做IIFE,即立即执行函数表达式。
catch作用域:
try{ var a = 1; console.log(b);//ReferenceError }catch(e){ var b = 2; console.log(e); } console.log(a);//1 console.log(b);//ReferenceError
let(es6)可以将变量绑定到任意的作用域中,通常是{...}中,且其声明不会被提升,使用let可以将变量限定在局部逻辑中,更加利于垃圾收集,下面的代码没有使用let声明someReallyBigData,而且someReallyBigData只会被process临时使用,而由于click形成了一个覆盖整个作用域的闭包,所以js引擎可能在process执行完后,还会一直保留someReallyBigData。
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 );
为了提高垃圾收集效率,可以将someReallyBigData使用let声明,并放在代码块中:
function process(data) { // 在这里做点有趣的事情 } // 在这个块中定义的内容可以销毁了! { let someReallyBigData = { .. }; process( someReallyBigData ); } var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt){ console.log("button clicked"); }, /*capturingPhase=*/false );
let在循环的使用中也是非常重要的:
for (let i=0; i<10; i++) { console.log( i ); } console.log( i ); // ReferenceError
上面这段代码相当于下面代码,每次迭代时进行重新绑定(这对于闭包是非常重要的):
{ let j; for (j=0; j<10; j++) { let i = j; // 每个迭代重新绑定! console.log( i ); } }
const(es6)与let类似,不同的是其值不可变:
{ const a = 1;//必须在声明时初始化 var b = 2; console.log(a);//1 try{ a = 2; }catch(e){ console.log(e);//TypeError } } console.log(b);//2 console.log(a);//ReferenceError