您现在的位置是:亿华云 > 应用开发

还在为 Not Defined 而苦恼吗?

亿华云2025-10-03 18:43:35【应用开发】5人已围观

简介书写 JavaScript 语言时,是否经常见到这种提示报错 * is not defined?是否经常出现 undefined?这些都是因为此时变量的访问是无效或者不可用的,而限定变量的可用性的代码

书写 JavaScript 语言时,而苦

是而苦否经常见到这种提示报错 * is not defined?

是否经常出现 undefined?

这些都是因为此时变量的访问是无效或者不可用的,而限定变量的而苦可用性的代码范围的就是这个变量的作用域。那什么是而苦作用域呢?

作用域

编程语言最基本的就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改,而苦而作用域就是而苦变量与函数的可访问范围。

作用域共有两种主要的而苦工作模型,词法作用域(静态作用域)和动态作用域:

词法作用域:作用域在定义时确认的而苦,即写代码时将变量和块作用域写在哪里来决定的而苦,JavaScript 使用的而苦就是词法作用域。动态作用域:作用域在运行时确定的而苦,源码下载比如 bash、而苦Perl。而苦JavaScript 一共有三种作用域:全局作用域:代码最外层。而苦函数作用域:创建一个函数就创建了一个作用域,而苦无论你调用不调用,函数只要创建了,它就有独立的作用域。ES6 的块级作用域:ES6 引入了 let 和 const 关键字和 { } 结合从而使 JavaScript 拥有了块级作用域,下面会详细介绍。

全局作用域

最外层是全局作用域,在脚本的任意位置都可以访问到,拥有全局作用域的变量也被称为“全局变量”。

下面看下哪些变量拥有全局作用域:

浏览器中,全局作用域中有一个全局对象 window,可以直接使用。// 获取窗口的文档显示区的高度

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

const 和 let 是一样的,也有“暂时性死区”,只是有以下限制:

const声明变量的时候必须同时初始化为某个值,且不能重新赋值。https://back-media.51cto.com/editor?id=702422/h6e90be6-0eG33HhD

赋值为对象的 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 // 3var、let、const差别差别

差别

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 引擎为每个迭代循环声明了一个新的变量,每个超时回调调用的都是不同的变量实例。

// 遍历对象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

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 对作用域的影响

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() 定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在 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内部

作用域链

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。作用域嵌套的查询规则如下:

首先,JS 引擎从当前的执行作用域开始查找变量。然后,如果找不到,引擎会在外层嵌套的作用域中继续查找。最后,直到找到该变量,或抵达最外层的全局作用域为止。

这样由多个作用域构成的链表就叫做作用域链。

例如:

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,有即获取传进作用域的值 3此时获取 a 的值,继续查找 b 的值,查找 add 函数作用域,查询是否有 b,没有查找上层作用域 func,查询是否有 b,有即获取当前作用域的值 2此时获取到 b 的值之后,再查找 c 的值,在add 函数作用域查询不到 c查找上层作用域 func,依然查询不到 c再往上一层作用域查找,即全局作用域,查询 a,查询到则获取作用域的值 1返回 6

闭包

不知道大家有没有注意到,之前说 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 指针指向的还是 addadd 依然持有对 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。

总结

作用域决定着变量的可访问范围,代码随处可见,了解作用域,避免使用访问不到的变量,减少文章开头的报错,代码质量直线上升哦。

参考资料

《你不知道的 JavaScript》《JaveScript 高级程序设计》

陈晨,微医第一利润中心前端组,一位“生命在于静止”的程序员。

很赞哦!(91262)