作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
分两种:
-- 静态作用域(词法作用域),函数的作用域在函数定义的时候就决定了
-- 动态作用域,函数的作用域是在函数调用的时候才决定的
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
JavaScript作用域
JavaScript 中主要有三种作用域:全局作用域、函数作用域和块作用域。查找变量时,会从当前作用域开始,逐级向上查找,直到找到为止。
- 全局作用域
全局作用域是最外层的作用域,存在于整个程序中。在全局作用域中声明的变量和函数可以在程序的任何地方访问。
- 函数作用域
函数作用域是指在函数中声明的变量和函数,只能在该函数内部访问。函数作用域是JavaScript中最常见的作用域。
- 块作用域
ES6引入了 let 和 const 关键字,它允许开发者创建块级作用域。
块级作用域是在代码块( if 语句, for 循环等)内部定义的作用域。
let 和 const 生命的变量只能在他们被定义的块级作用域内访问。
var scope = "global scope"
function checkscope(){
var scope = "local scope"
function f(){
return scope
}
return f()
}
checkscope () // local scope
var scope = "global scope"
function checkscope(){
var scope = "local scope"
function f(){
return scope
}
return f
}
checkscope()() // local scope
JavaScript 采用词法作用域,函数的作用域在函数定义时候就决定了,所以结果都是“local scope”。
变量提升(Hoisting)
变量提升是指JavaScript引擎在编译阶段将变量和函数的声明提升到其所在作用域的顶部。注意: 只有声明会被提升,而赋值不会被提升。
- var 声明的变量提升
console.log(str) // undefined
var str = "hello world"
console.log(str) // "hello world"
解读:在这个例子中, str 的赋值语句在 console.log 之后,但由于变量提升, str 实际上在编译阶段已经被提升到全局作用域顶部。因此,第一次 console.log(str) 输出的是 undefined 。
- 函数提升
函数提升不仅包括声明的提升,还包括函数体的提升。这意味着可以在函数声明之前就调用该函数。
console.log(add(1,2))
function add(a,b){
return a+b
}
// 3
解读:add 函数的定义在调用之后,但由于函数提升,add 函数在编译阶段已经被升到了顶部,因此可以正常使用。
- let 和 const 不会提升
与 var 声明不同, let 和 const 声明的变量不会被提升。如果在声明之前就使用这些变量,会抛出错误。
console.log(add(1,2))
let add = function (a,b){
return a+b
}
// 会报错
var、let、const区别?
- 作用域
- var是函数作用域或者全局作用域
在函数内部声明的var变量仅在该函数内有效;若在块级(如if或for)中声明,变量会泄漏到外层作用域。
if (true) {
var a = 10;
}
console.log(a); // 10(泄漏到全局)
- let / const 块级作用域
仅在大括号{}内有效,例如在for循环中声明的变量不会污染外层作用域。
if (true) {
let b = 20;
}
console.log(b); // ReferenceError: b未定义
- 重复声明
var允许在同一作用域内重复声明,后声明的变量会覆盖前者。
var c = 1;
var c = 2; // 合法
let/const:同一作用域内不可重复声明
let d = 3;
let d = 4; // SyntaxError: 标识符已声明
- 变量提升
var声明会被提升到作用域顶部,但初始值为undefined。
console.log(e); // undefined
var e = 5;
let/const:存在 暂时性死区(TDZ) ,声明前访问会报错
console.log(f); // ReferenceError: f未初始化
let f = 6;
- 重新赋值
var/let:可重新赋值
let g = 7;
g = 8; // 合法
const:声明时必须初始化,且不可重新赋值(但对象属性可修改)
const h = { name: "Alice" };
h.name = "Bob"; // 合法(修改属性)
h = {}; // TypeError: 常量不可重新赋值
- 初始化要求
var/let可以只声明,而后赋值
var a;
let b;
console.log(a,b) // undefined undefined
a = 'a';
b = 'b';
console.log(a,b) // a b
const必须声明同时赋值
const a;
console.log(a) // 报错
cons b = 'b'
console.log(b) // 'b'
- 全局作用域下行为
var:在全局作用域声明的变量会成为window对象的属性
var i = 9;
console.log(window.i); // 9
let/const:不会绑定到window对象
let j = 10;
console.log(window.j); // undefined
总结:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数/全局作用域 | 块级作用域 | 块级作用域 |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 变量提升 | 提升(初始undefined) | 暂时性死区(报错) | 暂时性死区(报错) |
| 重新赋值 | 允许 | 允许 | 不允许(对象属性可修改) |
| 初始化要求 | 可选 | 可选 | 必须初始化 |
| 全局绑定 | 是 | 否 | 否 |
为什么const可以修改对象属性值?
这里需要了解‘基本数据类型’和‘引用数据类型’
- 基本数据类型
基本数据类型包括:Number、String、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES11)。
基本数据类型的变量保存在栈区中,基本数据类型的值直接在栈内存中存储,值与值之间是独立存在的,修改一个变量不会影响其他的变量。
- 引用数据类型
引用数据类型包括:Object(含 Array、Function、Date、RegExp 等)。
引用数据类型的值是同时保存在栈内存和堆内存的对象,栈区保存了对象在堆区的地址,如下:
const person1 = {
id:1,
name:'student1',
};
const person2 = {
id:2,
name:'student2',
}
const arr = [
person1,
person2,
]
console.log(arr) // [{id:1,name:'student1'},{id:2,name:'student2'}]
person1.name = 'student3'
console.log(arr) // [{id:1,name:'student3'},{id:2,name:'student2'}]
| 栈区 | 堆区 | ||
|---|---|---|---|
| person1 | 堆区地址 | ----------> | object1---{} |
| person2 | 堆区地址 | ----------> | object2---{} |
| arr | 堆区地址 | ----------> | object3---[] |
const 声明的只是栈区内内容不变,基本数据类型保存在栈区中不可改变;引用数据类型在栈区内保存的地址不可改变,堆区内实际属性可以改变。
所以const声明的基本数据类型不允许重新赋值;引用数据类型不可以修改栈区内保存的堆区地址,可以修改堆区内的数据。
注意: 数组允许修改堆内数据(增删),禁止改变堆区地址(重新赋值)。