JS简记-作用域2
直觉上会认为JavaScript代码在执行时是由上到下一行一行执行的,但实际上这并不完全正确。
console.log(a);//ReferenceError
a = 2; var a; console.log( a );//2
console.log( a );//undefined var a = 2;
正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;和a = 2;。
第一个定义声明是在编译阶段进行的(即将变量声明作用于对应的作用域内)。
第二个赋值声明会被留在原地等待执行阶段。
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面,这个过程就叫作提升。
再比如下面的代码,var foo被提升,执行foo()时,foo还是undefined,对undefined进行函数调用自然是TyepError。
foo(); // 不是ReferenceError, 而是TypeError! var foo = function bar() { // ... }; bar();//ReferenceError,由于bar是具名函数表达式,bar标识符只被声明在函数内部,其实就相当于function(){var bar = ...self...}
变量声明和函数声明都会被提升,且函数声明先于变量声明被提升:
foo();//1 var foo;//虽然先于foo函数被声明,但foo函数被优先提升,当编译器声明foo变量时,发现作用域内已经有一个foo被声明,所以会忽略变量foo的声明,这很好的避免了无意中覆盖函数声明。 function foo(){console.log(1);} bar();//TypeError,注意优先提升的只是函数声明,而不是函数表达式。 var bar = function(){//...}
闭包
js中的闭包无处不在,闭包是基于词法作用域书写代码时所产生的自然结果。
闭包就是当函数离开其原声明时所在的作用域时,该函数仍然会拥有原父级作用域的访问权限,就好像该函数被原父级作用域包起来一样。
来看下面这个经典的循环示例来解释闭包:
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i );//5个6 }, i*1000 ); }
解决方案一:
for (var i=1; i<=5; i++) { (function() { var j = i; setTimeout( function timer() { console.log( j ); }, j*1000 ); })(); } or for (var i=1; i<=5; i++) { (function() { setTimeout( function timer() { console.log( j ); }, j*1000 ); })(i); }
解决方案二:
for (var i=1; i<=5; i++) { let j = i; // 是的,闭包的块作用域! setTimeout( function timer() { console.log( j ); }, j*1000 ); } for (let i=1; i<=5; i++) {//let指出变量在循环过程中不止被声明一次,每次迭代都会声明,这里便存在5个互相独立的闭包。 setTimeout( function timer() { console.log( i ); }, i*1000 ); }
利用闭包实现模块模式:
function Module(){ var e = true; function helloE(name){ var helloTxtE = "hello "; console.log(helloTxtE + name); } function helloC(name){ var helloTxtC = "你好 "; console.log(helloTxtC + name); } function changeLanguage(){ e = !e; if (e) { publicApi.hello = helloE; }else{ publicApi.hello = helloC; } } var publicApi = { hello: helloE, changeLanguage: changeLanguage }; return publicApi; } var m = Module();//如果要实现单例,只需要对Module使用IIFE m.hello("zyong");//hello zyong m.changeLanguage(); m.hello("zyong");//你好 zyong
模块依赖管理器:
function HelloModule(){ var e = true; function helloE(name){ var helloTxtE = "hello "; console.log(helloTxtE + name); } function helloC(name){ var helloTxtC = "你好 "; console.log(helloTxtC + name); } function changeLanguage(){ e = !e; if (e) { publicApi.hello = helloE; }else{ publicApi.hello = helloC; } } var publicApi = { hello: helloE, changeLanguage: changeLanguage }; return publicApi; } var ModuleManager = (function ModuleManager(){ var 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]; } var api = { define: define, get: get } return api; })(); ModuleManager.define("helloModule", [], HelloModule); var helloModule = ModuleManager.get("helloModule"); helloModule.hello("zyong"); helloModule.changeLanguage(); helloModule.hello("zyong");
最后,编程语言对变量和函数的声明分为词法作用域和动态作用域,js属于前者。词法作用域将作用域限定在了代码书写位置,其关注的是在何处声明;动态作用域中的作用域根据代码执行过程中的调用栈查找变量,其关注在何处运行。js中的this关键字就有点儿类似于动态作用域(但不是)。