前言
前些日子,在掘金上看到一片热门文章《在酷家乐做面试官的日子》。该文作者以面试官的角度,详细阐述了作为一名 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 world
f2('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