本文具体探讨了 JavaScript 中作用域、执行上下文、不同作用域下变量晋升与函数晋升的体现、顶层对象以及如何防止创立全局对象等内容。
变量作用域与晋升
在 ES6 之前,JavaScript 中只存在着函数作用域;而在 ES6 中,JavaScript 引入了 let、const 等变量申明关键字与块级作用域,在不同作用域下变量与函数的晋升体现也是不统一的。在 JavaScript 中,所有绑定的申明会在控制流达到它们呈现的作用域时被初始化;这里的作用域其实就是所谓的执行上下文(Execution Context),每个执行上下文分为内存调配(Memory Creation Phase)与执行(Execution)这两个阶段。在执行上下文的内存调配阶段会进行变量创立,即开始进入了变量的生命周期;变量的生命周期蕴含了申明(Declaration phase)、初始化(Initialization phase)与赋值(Assignment phase)过程这三个过程。
传统的 var 关键字申明的变量容许在申明之前应用,此时该变量被赋值为 undefined;而函数作用域中申明的函数同样能够在申明前应用,其函数体也被晋升到了头部。这种个性体现也就是所谓的晋升(Hoisting);尽管在 ES6 中以 let 与 const 关键字申明的变量同样会在作用域头部被初始化,不过这些变量仅容许在理论申明之后应用。在作用域头部与变量理论申明处之间的区域就称为所谓的临时死域(Temporal Dead Zone),TDZ 可能防止传统的晋升引发的潜在问题。另一方面,因为 ES6 引入了块级作用域,在块级作用域中申明的函数会被晋升到该作用域头部,即容许在理论申明前应用;而在局部实现中该函数同时被晋升到了所处函数作用域的头部,不过此时被赋值为 undefined。
作用域
作用域(Scope)即代码执行过程中的变量、函数或者对象的可拜访区域,作用域决定了变量或者其余资源的可见性;计算机平安中一条根本准则即是用户只应该拜访他们须要的资源,而作用域就是在编程中遵循该准则来保障代码的安全性。除此之外,作用域还可能帮忙咱们晋升代码性能、追踪谬误并且修复它们。JavaScript 中的作用域次要分为全局作用域(Global Scope)与部分作用域(Local Scope)两大类,在 ES5 中定义在函数内的变量即是属于某个部分作用域,而定义在函数外的变量即是属于全局作用域。
全局作用域
当咱们在浏览器控制台或者 Node.js 交互终端中开始编写 JavaScript 时,即进入了所谓的全局作用域:
// the scope is by default globalvar name = 'Hammad';
定义在全局作用域中的变量可能被任意的其余作用域中拜访:
var name = 'Hammad';console.log(name); // logs 'Hammad'function logName() { console.log(name); // 'name' is accessible here and everywhere else}logName(); // logs 'Hammad'
函数作用域
定义在某个函数内的变量即从属于以后函数作用域,在每次函数调用中都会创立出新的上下文;换言之,咱们能够在不同的函数中定义同名变量,这些变量会被绑定到各自的函数作用域中:
// Global Scopefunction someFunction() { // Local Scope #1function someOtherFunction() { // Local Scope #2 }}// Global Scopefunction anotherFunction() { // Local Scope #3}// Global Scope
函数作用域的缺点在于粒度过大,在应用闭包或者其余个性时导致异样的变量传递:
var callbacks = [];// 这里的 i 被晋升到了以后函数作用域头部for (var i = 0; i <= 2; i++) { callbacks[i] = function () {return i * 2; };}console.log(callbacks[0]()); //6console.log(callbacks[1]()); //6console.log(callbacks[2]()); //6
块级作用域
相似于 if、switch 条件抉择或者 for、while 这样的循环体即是所谓的块级作用域;在 ES5 中,要实现块级作用域,即须要在原来的函数作用域上包裹一层,即在须要限度变量晋升的中央手动设置一个变量来代替原来的全局变量,譬如:
var callbacks = [];for (var i = 0; i <= 2; i++) { (function (i) { // 这里的 i 仅归属于该函数作用域 callbacks[i] = function () {return i * 2; }; })(i);}callbacks[0]() === 0;callbacks[1]() === 2;callbacks[2]() === 4;
而在 ES6 中,能够间接利用 let 关键字达成这一点:
let callbacks = []for (let i = 0; i <= 2; i++) { // 这里的 i 属于以后块作用域 callbacks[i] = function () { return i * 2 }}callbacks[0]() === 0callbacks[1]() === 2callbacks[2]() === 4
词法作用域
词法作用域是 JavaScript 闭包个性的重要保障,一般来说,在编程语言里咱们常见的变量作用域就是词法作用域与动静作用域(Dynamic Scope),绝大部分的编程语言都是应用的词法作用域。词法作用域重视的是所谓的 Write-Time,即编程时的上下文,而动静作用域以及常见的 this 的用法,都是 Run-Time,即运行时上下文。词法作用域关注的是函数在何处被定义,而动静作用域关注的是函数在何处被调用。JavaScript 是典型的词法作用域的语言,即一个符号参照到语境中符号名字呈现的中央,局部变量缺省有着词法作用域。此二者的比照能够参考如下这个例子:
function foo() { console.log( a ); // 2 in Lexical Scope ,But 3 in Dynamic Scope}function bar() {var a = 3; foo();}var a = 2;bar();
执行上下文与晋升
作用域(Scope)与上下文(Context)经常被用来形容雷同的概念,不过上下文更多的关注于代码中 this 的应用,而作用域则与变量的可见性相干;而 JavaScript 标准中的执行上下文(Execution Context)其实形容的是变量的作用域。家喻户晓,JavaScript 是单线程语言,同时刻仅有单任务在执行,而其余工作则会被压入执行上下文队列中;每次函数调用时都会创立出新的上下文,并将其增加到执行上下文队列中。
执行上下文
每个执行上下文又会分为内存创立(Creation Phase)与代码执行(Code Execution Phase)两个步骤,在创立步骤中会进行变量对象的创立(Variable Object)、作用域链的创立以及设置以后上下文中的 this 对象。所谓的 Variable Object ,又称为 Activation Object,蕴含了以后执行上下文中的所有变量、函数以及具体分支中的定义。当某个函数被执行时,解释器会先扫描所有的函数参数、变量以及其余申明:
'variableObject': { // contains function arguments, inner variable and function declarations}
在 Variable Object 创立之后,解释器会持续创立作用域链(Scope Chain);作用域链往往指向其副作用域,往往被用于解析变量。当须要解析某个具体的变量时,JavaScript 解释器会在作用域链上递归查找,直到找到适合的变量或者任何其余须要的资源。作用域链能够被认为是蕴含了其本身 Variable Object 援用以及所有的父 Variable Object 援用的对象:
'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts}
而执行上下文则能够表述为如下形象对象:
executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis}
变量的生命周期与晋升
变量的生命周期蕴含着变量申明(Declaration Phase)、变量初始化(Initialization Phase)以及变量赋值(Assignment Phase)三个步骤;其中申明步骤会在作用域中注册变量,初始化步骤负责为变量分配内存并且创立作用域绑定,此时变量会被初始化为 undefined,最初的调配步骤则会将开发者指定的值调配给该变量。传统的应用 var 关键字申明的变量的生命周期如下:
而 let 关键字申明的变量生命周期如下:
如上文所说,咱们能够在某个变量或者函数定义之前拜访这些变量,这即是所谓的变量晋升(Hoisting)。传统的 var 关键字申明的变量会被晋升到作用域头部,并被赋值为 undefined:
// var hoistingnum; // => undefined var num; num = 10; num; // => 10 // function hoistinggetPi; // => function getPi() {...} getPi(); // => 3.14 function getPi() { return 3.14;}
变量晋升只对 var 命令申明的变量无效,如果一个变量不是用 var 命令申明的,就不会产生变量晋升。
console.log(b);b = 1;
下面的语句将会报错,提醒 ReferenceError: b is not defined,即变量 b 未声明,这是因为 b 不是用 var 命令申明的,JavaScript 引擎不会将其晋升,而只是视为对顶层对象的 b 属性的赋值。ES6 引入了块级作用域,块级作用域中应用 let 申明的变量同样会被晋升,只不过不容许在理论申明语句前应用:
> let x = x;ReferenceError: x is not defined at repl:1:9 at ContextifyScript.Script.runInThisContext (vm.js:44:33) at REPLServer.defaultEval (repl.js:239:29) at bound (domain.js:301:14) at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:433:10) at emitOne (events.js:120:20) at REPLServer.emit (events.js:210:7) at REPLServer.Interface._onLine (readline.js:278:10) at REPLServer.Interface._line (readline.js:625:8)> let x = 1;SyntaxError: Identifier 'x' has already been declared
函数的生命周期与晋升
根底的函数晋升同样会将申明晋升至作用域头部,不过不同于变量晋升,函数同样会将其函数体定义晋升至头部;譬如:
function b() { a = 10; return; function a() {} }
会被编译器批改为如下模式:
function b() {function a() {} a = 10;return;}
在内存创立步骤中,JavaScript 解释器会通过 function 关键字辨认出函数申明并且将其晋升至头部;函数的生命周期则比较简单,申明、初始化与赋值三个步骤都被晋升到了作用域头部:
如果咱们在作用域中反复地申明同名函数,则会由后者笼罩前者:
sayHello();function sayHello () {function hello () { console.log('Hello!'); } hello();function hello () { console.log('Hey!'); }}// Hey!
而 JavaScript 中提供了两种函数的创立形式,函数申明(Function Declaration)与函数表达式(Function Expression);函数申明即是以 function 关键字开始,跟随者函数名与函数体。而函数表达式则是先申明函数名,而后赋值匿名函数给它;典型的函数表达式如下所示:
var sayHello = function() { console.log('Hello!');};sayHello();// Hello!
函数表达式遵循变量晋升的规定,函数体并不会被晋升至作用域头部:
sayHello();function sayHello () {function hello () { console.log('Hello!'); } hello();var hello = function () { console.log('Hey!'); }}// Hello!
在 ES5 中,是不容许在块级作用域中创立函数的;而 ES6 中容许在块级作用域中创立函数,块级作用域中创立的函数同样会被晋升至以后块级作用域头部与函数作用域头部。不同的是函数体并不会再被晋升至函数作用域头部,而仅会被晋升到块级作用域头部:
f; // Uncaught ReferenceError: f is not defined(function () { f; // undefined x; // Uncaught ReferenceError: x is not definedif (true) { f(); let x;function f() { console.log('I am function!'); } }}());
防止全局变量
在计算机编程中,全局变量指的是在所有作用域中都能拜访的变量。全局变量是一种不好的实际,因为它会导致一些问题,比方一个曾经存在的办法和全局变量的笼罩,当咱们不晓得变量在哪里被定义的时候,代码就变得很难了解和保护了。在 ES6 中能够利用 let关键字来申明本地变量,好的 JavaScript 代码就是没有定义全局变量的。在 JavaScript 中,咱们有时候会无意间创立出全局变量,即如果咱们在应用某个变量之前忘了进行申明操作,那么该变量会被主动认为是全局变量,譬如:
function sayHello(){ hello = "Hello World";return hello;}sayHello();console.log(hello);
在上述代码中因为咱们在应用 sayHello 函数的时候并没有申明 hello 变量,因而其会创立作为某个全局变量。如果咱们想要防止这种偶尔创立全局变量的谬误,能够通过强制应用 strict mode来禁止创立全局变量。
函数包裹
为了防止全局变量,第一件事件就是要确保所有的代码都被包在函数中。最简略的方法就是把所有的代码都间接放到一个函数中去:
(function(win) { "use strict"; // 进一步防止创立全局变量var doc = window.document; // 在这里申明你的变量 // 一些其余的代码}(window));
申明命名空间
var MyApp = { namespace: function(ns) {var parts = ns.split("."), object = this, i, len;for(i = 0, len = parts.lenght; i < len; i ++) {if(!object[parts[i]]) { object[parts[i]] = {}; } object = object[parts[i]]; }return object; }};// 定义命名空间MyApp.namespace("Helpers.Parsing");// 你当初能够应用该命名空间了MyApp.Helpers.Parsing.DateParser = function() { //做一些事件};
模块化
另一项开发者用来防止全局变量的技术就是封装到模块 Module 中。一个模块就是不须要创立新的全局变量或者命名空间的通用的性能。不要将所有的代码都放一个负责执行工作或者公布接口的函数中。这里以异步模块定义 Asynchronous Module Definition (AMD) 为例,更具体的 JavaScript 模块化相干常识参考 JavaScript 模块演变简史
//定义define( "parsing", //模块名字 [ "dependency1", "dependency2" ], // 模块依赖 function( dependency1, dependency2) { //工厂办法 // Instead of creating a namespace AMD modules // are expected to return their public interface var Parsing = {}; Parsing.DateParser = function() { //do something };return Parsing; });// 通过 Require.js 加载模块require(["parsing"], function(Parsing) { Parsing.DateParser(); // 应用模块});