前言
前些日子,在掘金上看到一片热门文章。该文作者以面试官的角度,详细阐述了作为一名 web 应聘者应该具有哪些技能,才会更让人青睐。
在对比自身的过程中,发现有一些问题,或许了解,但不全面,这也是本系列文章诞生的缘由。
什么是作用域
在 JavaScript 中,作用域为可访问变量,对象,函数的集合。 根据作用域范围,又分为全局作用域(Global)和局部作用域(Local)
什么是全局作用域
全局作用域(Global)是贯彻应用程序整个声明周期的的可访问变量,对象,函数的集合,换言之,只要它没有被删除或被覆盖,我们便可以在任何位置、任何时间使用它。
例如 window 这个内置的 JavaScript 对象就是一个全局对象,拥有全局作用域。
我们可以方便的创建一个全局对象(但切记不要滥用全局作用域,它可能会对应用的性能产生影响,也同样容易被其它使用者覆盖)。
/* * 在函数外部创建的变量、对象、函数都是全局的,拥有全局作用域 * msg1 就是一个全局变量,可在 func1 函数内部使用 */var msg1 = 'hello world'var func1 = function(){ console.log(msg1)}func1() => hello world复制代码
什么是局部作用域
小贴士: 局部作用域也叫函数作用域
局部作用域(Local)是指那些在函数内部声明的变量,对象,函数的集合,局部作用域只在当前上下文有效。
/* * 在函数外部创建的变量、对象、函数都是全局的,拥有全局作用域 * msg1 就是一个全局变量,可在 func1 函数内部使用 */var msg1 = 'hello world'var func1 = function(){ /* * 在函数内部创建的变量、对象、函数都是局部的,拥有局部作用域 * msg2 就是一个局部变量,只能在 func1 函数内部使用 */ var msg2 = 'hello world !!!' console.log(msg2)}func1() => hello world !!!// 当我们视图打印 func1 内部的 msg2 时,提示 msg2 未声明。console.log(msg2) => Uncaught ReferenceError: msg2 is not defined复制代码
局部作用域 - 变量提升的大坑
思考以下代码:
/* * func1 出人意料的结果!!! * '判断开始' 为什么不是应该输出 a is not defined * '判断结束' 为什么不是应该输出 a is not defined */var func1 = function(){ console.log('判断开始:' + a) if(1 === 1){ var a = 1 console.log('判断语句:' + a) } console.log('判断结束:' + a)}func1() => 判断开始:undefined判断语句:1判断结束:1复制代码
JavaScript 中,函数及变量的声明(而非赋值)都将被提升到函数的最顶部
好吧,被打败了。依照这个原则,我们看看这个函数,到底是怎么执行的。
/* * 因为存在变量提升原则,函数及变量的声明都将被提升到函数的最顶部 * 因此 var a = 1 实际在函数开头就被定义好了 * 这样看起来就解释的通了 */var func1 = function(){ var a console.log('判断开始:' + a) if(1 === 1){ a = 1 console.log('判断语句:' + a) } console.log('判断结束:' + a)}func1() => 判断开始:undefined判断语句:1判断结束:1复制代码
所以,前人在填补了无数这样的坑之后,总结了一个道理:
请一定一定在函数开头,定义变量。
局部作用域 - 块级作用域
在早期的 JavaScript 语法中,是没有块级作用域的。在 ES6 之后,引入了 const 和 let 关键字。const 和 let 的引入帮我们解决了变量提升导致的隐形问题。
回到刚才的例子:
/* * func1 终于正常了的结果 * let 关键字声明的变量,称之为块级作用域。 * 也就是说,在该变量声明之前,是没有办法访问它的。 */var func1 = function(){ console.log('判断开始:' + a) if(1 === 1){ // 使用 let 关键字声明变量 let a = 1 console.log('判断语句:' + a) } console.log('判断结束:' + a)}func1() => Uncaught ReferenceError: a is not defined复制代码
let 和 const 关键字声明的变量,也是局部变量,但它的作用域不是局部作用域,而是块级作用域。它的作用域绑定变量声明的区域不受外部影响,也不能被外部使用。
在语法上,称之为“暂时性死区”
局部作用域 - 词法作用域(闭包)
在局部作用域中,还有一类是词法作用域(闭包)
/* * 这是一个典型的闭包结构 * 在函数 func1 内部,定义了另外一个函数,这个函数的作用是输出 x 和 y 相加的字符串 */var func1 = function(x){ return function(y){ console.log(x + ' ' + y) }}/* * 得益于闭包的定义规则(闭包就是创建一个了上下文环境,这个环境包含了创建时所能访问的所有局部变量) * f1 和 f2 共享同一个函数定义,却拥有各自不同的上下文环境 */var f1 = func1('hello')var f2 = func1('HELLO')f1('world') => hello worldf2('WORLD') => HELLO WORLD复制代码
什么是作用域链
谈到作用域,不得不谈到作用域链。
什么是作用域链?
作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问
用人话来说就是:
当一个函数执行时,实际上存在一些的规则,这些规则指明当前函数能够如何访问到有权限变量、对象和函数。
我们通过一段代码来理解作用域链:
/* * 函数有一个内部属性 [[scope]],当函数创建的时候,就会保存自己和所有父变量对象到其中 * 当 func1() 运行时。 * func2 的 [[scope]] = func2.[[scope]] 和 func1.[[scope]] 和 global.[[scope]] * 因此 func2 实际计算的是 func2.[[scope]].msg2 + func1.[[scope]].msg1 + global.[[scope]].msg */ var msg = '0'var func1 = function(){ var msg1 = '1' var func2 = function(){ var msg2 = '2' return msg + msg1 + msg2 } return func2()}func1() => 012复制代码