陈晨,微医第一利润中心前端组,一位“生命在于静止”的程序员。
书写 JavaScript
语言时,
是否经常见到这种提示报错 * is not defined
?
是否经常出现 undefined
?
这些都是因为此时变量的访问是无效或者不可用的,而限定变量的可用性的代码范围的就是这个变量的作用域。那什么是作用域呢?
编程语言最基本的就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改,而作用域就是变量与函数的可访问范围。
作用域共有两种主要的工作模型,词法作用域(静态作用域)和动态作用域:
JavaScript
使用的就是词法作用域。bash
、Perl
。JavaScript
一共有三种作用域:
ES6
引入了 let
和 const
关键字和 {}
结合从而使 JavaScript
拥有了块级作用域,下面会详细介绍。最外层是全局作用域,在脚本的任意位置都可以访问到,拥有全局作用域的变量也被称为“全局变量”。
下面看下哪些变量拥有全局作用域:
// 获取窗口的文档显示区的高度
window.innerHeight
var a = 1
console.log(window.a) // 1 --- var 声明的 a 成为了 window 的属性,为全局变量
function func1 () {
console.log('hello')
}
func1() // hello
window.func1() // hello
window
对象上。b = 2 // 全局变量
function func1 () {
c = 2
}
func1()
console.log(window.b) // 2
console.log(window.c) // 2
省略了关键字的变量,不管是函数外面的 b
还是函数里面的 c
都是全局变量,且挂载在 window
上,但是这种省略关键字是不规范和不利于维护的 ,不推荐使用。
把上面的 a
代码反过来如下:
console.log(window.a) // 输出 undefined
var a = 1
这个时候 a
在声明之前是可访问的,只是输出了 undefined
,即为经常提到的“变量提升”。
变量提升:var 关键字声明的变量,无论实际声明的位置在何处,都会被视为声明在当前作用域的顶部(包括在函数和全局作用域)
因为 JS 引擎的工作方式是分为编译和执行两个阶段:
所以下面两段代码是等价的:
console.log(a); // 输出undefined
var a =1;
// 等价于
var a;
console.log(a); // 输出undefined
a =1;
var a = 1
,编译器遇到 var a
会在作用域中声明新的变量 a
,console.log(a)
和 a = 1
a
(此时是 undefined
) 和给 a
赋值1函数作用域内的变量或者内部函数,作用域都是函数作用域,对外都是封闭的,从外层的作用域无法直接访问函数内部的作用域,否则会报引用错误异常。如下:
function func1 () {
var a = 1;
return a
}
func1() // 1 函数内部是能够访问的
console.log(a) // Uncaught ReferenceError: a is not defined
函数声明中,JS 引擎会在代码执行之前获取函数声明,并在执行上下文中生成函数定义。
console.log(add(10, 10)) // 正常返回20
function add (a, b) {
return a + b
}
代码正常运行,函数声明可以在任何代码执行之前先被读取并添加执行上下文,即函数声明提升(和前面的变量声明提升一样)。
函数表达式必须等待代码执行到那一行,才会在执行上下文中生成函数定义
console.log(add(10, 10)) // Uncaught TypeError: add is not a function
var add = function (a, b) {
return a + b
}
函数表达式 var add = function(){}
是变量声明提升。在这种情况下,add
是一个变量,因此这个变量的声明也将提升到顶部,而变量的赋值依然保留在原来的位置,所以此时的报错是变量 add
类型不对。
前面提到函数声明提升和变量声明提升,以及使用的现象,下面看一下两者共同使用的例子:
test() // “执行函数声明”
var test = function () {
console.log('执行函数表达式')
}
function test (a, b) {
console.log('执行函数声明')
}
test() // “执行函数表达式”
第一个 test()
输出“执行函数声明”,第二个 test()
输出“执行函数表达式”,是因为经历了函数声明提升和变量声明提升(函数提升优先于变量提升),代码等价于:
// 函数声明提升到顶部
function test (a, b) {
console.log('执行函数声明')
}
// 变量提升,变量提升不会覆盖(同名)函数提升,只有变量再次赋值时,才会被覆盖
var test
// 还在原处
test() // “执行函数声明”
test = function () {
console.log('执行函数表达式')
}
test() // “执行函数表达式”
ES6
新增的 let
和 const
作用域是块级作用域,由最近的一对花括号 {}
界定,以 let
为例如下:
{
var a = 1
let b = 2
}
console.log(a) // 1
console.log(b) // Uncaught ReferenceError: b is not defined
在花括号内使用 let
声明的变量,在外部是无法访问的,即块级作用域。
当使用 let
关键字声明的变量提前访问时:
{
console.log(a) // 报错 Uncaught ReferenceError: a is not defined
let a = 1
}
上述之所以报错是因为 let
有“暂时性死区”
暂时性死区:声明变量之前,该变量都是不可用的,只要进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
上述用代码可以等价理解为:
{
//let a ,暂时性死区开始的地方
console.log(a) // 由于 a = 2 在暂时性死区中,所以报错
a = 1 // 暂时性死区结束的地方
}
const
和 let
是一样的,也有“暂时性死区”,只是有以下限制:
const
声明变量的时候必须同时初始化为某个值,且不能重新赋值。<!--Uncaught SyntaxError: Missing initializer in const declaration -->
const a
const
变量不能再赋值其他的引用值,但是对象的键不受限制( Object.freeze()
可以完全冻结对象,键值对也不能修改)const a = 1
a = 1 // Uncaught TypeError: Assignment to constant variable.
// 对象
const obj = {
a: 1,
b: 2
}
obj.b = 3
console.log // 3
差别 | var | let | const |
---|---|---|---|
作用域 | 函数作用域 | 块作用域 | 块作用域 |
声明 | 同一个作用域可多次声明 | 同一个作用域不可多次声明 | 同一个作用域不可多次声明且要同时赋值,后续不可更改 |
特性 | 变量提升(且不加var是全局变量) | 暂时性死区 | 暂时性死区 |
常见例子:for 循环
for (var i = 0; i< 5; i++) {
setTimeout(function() {
console.log(i)
})
}
// 5 5 5 5 5
for (let i = 0; i< 5; i++) {
setTimeout(function() {
console.log(i)
})
}
// 0 1 2 3 4
var:全局变量,退出循环时迭代变量保存的是循环退出的时候的值,在执行超时回调的时候,所有的 i 都是同一个变量。
let:块作用域,JS 引擎为每个迭代循环声明了一个新的变量,每个超时回调调用的都是不同的变量实例。
const
不能修改值,所以不能使用 for
循环 i++
, const
的应用如下:
// 遍历对象key
for (const key in {a: 1, b: 1}) {
console.log(key) // a b
}
// 遍历数字
for (const val of [1, 2, 3]) {
console.log(val) // 1 2 3
}
eval
由于性能不好、不安全、代码逻辑混乱等各种问题,一般不支持在代码里使用它,但是还是要了解下的,用网友的话就是:可以远离它,但是要了解它
这个方法就是一个完整的 ES
解释器,它接收一个参数,
即一个要执行的 ES(JavaScript)
字符串,把对应的字符串解析成 JavaScript
代码并运行(将 json 的字符串解析成为 JSON 对象)。eval
的简单用法:
JavaScript
语句,那么就会执行这些语句eval("2 + 2") // 输出 4
eval("console.log('hi')") // 输出 hi
eval(new String("2 + 2")) // String {'2 + 2'}
eval
在 JavaScript
中有两种调用方式:直接调用和间接调用。
eval
内代码块的作用域绑定到当前作用域,直接使用 eval()
。function testEval () {
eval('var a = 111')
console.log(a) // 111
}
testEval()
console.log(a) // 报错
上面在 testEval 函数内部是可以获取到 a 的,所以 eval 修改了 testEval 函数作用域。
eval
内代码块的作用域绑定到全局作用域,使用 window.eval()
(IE8兼容性问题),window.execScript
(支持IE8及以下的版本),为了解决兼容性问题,也可以在全局赋值给变量,然后在函数内使用。// 有IE兼容问题
function testEval () {
window.eval('var a = 111')
console.log(a) // 111
}
testEval()
console.log(a) // 111 eval定义的变量绑定到了全局作用域
// 解决兼容性问题
var evalExp = eval
function testEval () {
evalExp('var a = 111')
console.log(a) // 111
}
testEval()
console.log(a) // 111 eval定义的变量绑定到了全局作用域
通过 eval()
定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在 eval()
执行的时候才会被创建。
下面是let
、var
和函数的不同效果,如下:
// 函数
sayHi() // error: sayHi is not defined,没有函数声明提升
eval("function sayHi() { console.log('hi'); }");
sayHi() // hi
// var
msg // error: msg is not defined,没有变量声明提升
eval("var msg = 'hello world'")
console.log(msg) // hello world
// let
eval("let msg = 'hello world';console.log(msg)") // // hello world
console.log(msg) // 报错 let 作用域只能是eval内部
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。作用域嵌套的查询规则如下:
这样由多个作用域构成的链表就叫做作用域链。
例如:
var c = 1
function func () {
var b = 2
function add (a) {
return a + b + c
}
return add
}
const addTest = func()
addTest(3) // 6
作用域链为:
执行funcTest()的时候:
add
函数作用域,查询是否有 a
,有即获取传进作用域的值 3a
的值,继续查找 b
的值,查找 add
函数作用域,查询是否有 b
,没有func
,查询是否有 b
,有即获取当前作用域的值 2b
的值之后,再查找 c
的值,在 add
函数作用域查询不到 cfunc
,依然查询不到 c不知道大家有没有注意到,之前说 JavaScript
作用域是在定义时确认的,即在定义的函数外面是访问不到函数里面的变量的,但是上面作用域嵌套的例子中,addTest
却能够访问到函数 func
的内部变量,这就是因为“闭包”的存在。
闭包就是函数内部定义的函数,可以记住并访问所在的作用域,即使函数是在当前词法作用域之外执行,也可以访问内部变量。
var c = 1
function func () {
var b = 2
function add (a) {
return a + b + c
}
return add
}
const addTest = func()
addTest(3) // 6
add()
的作用域能够访问 func()
的内部作用域func
,将内部函数 add
的引用赋值给外部的变量 addTest
,此时 addTest
指针指向的还是 add
add
依然持有对 func
作用域的引用,而这个引用就叫作闭包addTest
,即外部执行 add
,通过闭包能访问到定义时的作用域。使用闭包的时候原函数
func
不会被回收,还被包含在 add 的作用域里,因此会比其他函数占用更多的内存,容易造成内存泄漏。
如上可知,闭包在代码里随处可见,下面看下使用场景:
如上面所举的 let 循环的例子:
for (let i = 0; i< 5; i++) {
setTimeout(function() {
console.log(i)
})
}
setTimeout
的回调函数记住了当前的词法作用域,当循环结束,执行函数的时候,能够访问到当时的作用域的 i
。
// 获取数组中的正序和逆序排列
function arrOperate () {
let errorMsg = '请传入一个数组'
// 正序
function getPositiveArr(arr) {
if (Array.isArray(arr)) {
return arr.sort((a, b) => {
return a - b
})
} else {
throw errorMsg
}
}
// 逆序
function getBackArr(arr) {
if (Array.isArray(arr)) {
return arr.sort((a, b) => {
return b - a
})
} else {
throw errorMsg
}
}
return {
getPositiveArr,
getBackArr
}
}
const arrObj = arrOperate()
arrObj.getPositiveArr([1, 10, 5, 89, 46]) // [1, 5, 10, 46, 89]
arrObj.getBackArr([1, 10, 5, 89, 46]) // [89, 46, 10, 5, 1]
arrObj.getPositiveArr(123) // Uncaught 请传入一个数组
这个模式在 JavaScript
中被称为模块,arrOperate()
返回一个对象,包含对内部函数的引用,
而内部函数getPositiveArr()
和 getBackArr()
函数具有涵盖模块实例内部作用域的闭包,可访问 errorMsg
。
作用域决定着变量的可访问范围,代码随处可见,了解作用域,避免使用访问不到的变量,减少文章开头的报错,代码质量直线上升哦。
别忘记对我素质三连,点赞、关注、评论^_^
前往微医互联网医院在线诊疗平台,快速问诊,3分钟为你找到三甲医生。(https://wy.guahao.com/?channel=influence)