一、JS执行引擎
JavaScript代码需要在特定的执行环境中运行,主要分为两类:
- 浏览器环境:采用V8引擎
- Node.js环境
二、JS代码的执行过程
所有代码都会经历三个阶段:
- 词法分析:读取关键字、词法单元,将字符流转换为标记(token)序列
- 语法分析:生成抽象语法树(AST)
- 代码生成:转化为机器可执行的字节码
作为解释型语言,JS在执行时会即时编译并执行代码。
三、作用域
作用域其实就是词法访问的规则,作用域是可以随意嵌套的,正如函数里面可以再定义函数。
1. 作用域分类
- 全局作用域:最外层作用域,浏览器中绑定到window对象
- 函数作用域:函数形成的作用域
- 块级作用域:由{}包裹的结构(if/for/while等)配合let/const实现
谈到块级作用域,那就得先来聊聊var let const 的区别了。
2. var ,let ,const 的区别
-
var 声明的变量存在声明提升,就是将变量的声明提升到当前作用域的顶部。 而let 和const 不存在声明提升
-
var可以重复声明变量(后面声明的会覆盖前面的),let和const不可以重复声明变量
-
在浏览器环境下,全局最外层有一个window对象,全局也叫window。 在全局声明的var变量会默认添加到window对象上。在全局的var a = 1 相当于 window.a = 1。而let不会
-
const 和 let的唯一区别就是const声明的变量无法再次赋值(const声明的变量是个常量,无法修改)。
- 声明提升
console.log(a); // undefined
var a = 123;
// 等价于:
var a;
console.log(a);
a = 123;
只提升声明,不提升初始化
let/const
不存在声明提升)
- 值得注意的是, const 声明一个对象,是可以修改对象里面的属性的。
const
声明的变量会永久绑定到初始值的内存地址,这意味着允许修改引用类型的内容(可以通过属性或索引直接修改其内部状态),禁止重写变量的引用指向,任何试图将变量赋值为新值的操作都会报错。
const person = { name: 'Alice', age: 25 };
person.name = 'Bob'; // ✅ 允许(字符串不可变,但替换为新字符串)
person.age = 30; // ✅ 允许
person = {}; // ❌ 报错
- 总结
特性 | var | let/const |
---|---|---|
声明提升 | ✅ | ❌ |
重复声明 | ✅ | ❌ |
全局绑定 | ✅(附加window) | ❌ |
块级作用域 | ❌ | ✅ |
接下来再让我们来谈谈什么是块级作用域。
3.块级作用域
ES6引入let/const
实现真正的块级作用域,消除传统var
带来的变量提升和作用域污染问题
先来看以下代码
function createCounter() {
for (var i = 0; i < 3; i++) {
console.log(`第${i+1}次调用`);
}
return i; // 这里会返回什么呢?
}
const result = createCounter();
console.log(result); // 输出:3
console.log(i); // 直接输出:3(全局变量泄漏!)
var i
在函数作用域内声明,但实际上会提升到函数顶层,导致循环结束后 i
仍然存在于全局作用域中。这造成了全局污染,而将var改成let就可以修复这个问题,原因就是let和{}形成了块级作用域。
{
let blockVar = 'I am block scoped';
console.log(blockVar); // 正常输出
}
console.log(blockVar); // ReferenceError
let + {}
会形成块级作用域 。
花括号{}
既可以是if、for等语句上的{},也可以是{}
单独包裹的语句块。
在 {}
内部声明的 blockVar
只能在当前代码块内被访问,块外尝试访问时,变量已超出作用域范围,因此抛出错误。
4. 作用域查找规则
- 内层作用域可以访问外层作用域,外层作用域不能访问内层。
- 先在自己的作用域找,自己的找不到,再去外层作用域找
function createScope() {
const foo = 'foo';
return function bar() {
console.log(bar); // [Function: bar]
console.log(foo); // foo
console.log(this); // window/window对象
};
}
const instance = createScope();
instance();
作用域链:bar → createScope → 全局作用域
const globalVar = '我是全局变量';
function outer() {
const outerVar = '我是外层变量';
function inner() {
const innerVar = '我是内层变量';
console.log(innerVar); //内层可以访问外层,所以这三个都可以正常打印输出
console.log(outerVar);
console.log(globalVar);
}
inner(); // 调用内层函数
}
outer(); // 调用外层函数
// 全局作用域尝试访问内层变量
console.log(innerVar); // ❌ ReferenceError 外层不能访问内层
5.暂时性死区
当使用 let 或 const 声明变量形成了块级作用域时,在变量声明前访问该变量会报错,即使块级作用域外也声明了相同的变量,也无法访问,这就是暂时性死区。
JavaScript 引擎在执行代码时,会将 let
/const
声明的变量提前绑定到作用域(但保留初始化状态),在声明语句之前的区域形成“死区”。
let a = 1;
if (true) {
console.log(a); // TDZ: Cannot access 'a' before declaration
let a = 2;
}
尽管外部存在 a
,但在块内声明 let a
之前访问 a
仍会报错,因为:
- 块级作用域的变量绑定优先级高于外部作用域。
- 引擎在解析阶段已识别块内
a
的声明,导致在此前的所有代码中无法访问。
再来看以下代码,如果把if语句里的let a = 2删掉,这样就不会报错。
let a = 1;
if (true) {
console.log(a); // TDZ: Cannot access 'a' before declaration
}
由于块内没有 let a
声明,就不会形成块级作用域,沿作用域链向上查找 → 找到外层 a = 1
, 正常输出外层变量的值。
四、欺骗词法机制
1. eval()
eval(str)可以将里面的字符串处理成代码,这就相当于var b = 3, 申明了变量b。 具有欺骗性,把原本不属于这行的代码搬到了这行
function foo(str,a){
eval(str) // 相当于var b = 3
console.log(a,b); // 1 3
}
var b = 2
foo('var b = 3',1)
eval()执行动态生成的代码、 违背作用域规则
2. with语句
我们一般是obj.a=3这样来修改对象的属性,with语句也能用来修改对象身上的属性(尤其是批量修改的时候更方便)。 with语句只能修改已有的属性,没有的属性改不动,也不会添加到对象身上去
- with语句有一个很不好的副作用(bug),当你去改一个对象不存在的属性时,该属性会泄露到全局
- 当对象中没有属性x时,with修改x属性会导致x泄露到全局
const obj = {
a: 1,
b: 2
};
with(obj) {
a = 3; // 相当于 obj.a = 3
c = 4; // 全局新增变量c
}
console.log(obj.a); // 3
console.log(c); // 4