JS作用域与预解析
目录
作用域:
一、什么是作用域
(1)、全局作用域
(2)、局部作用域(函数作用域)
(3)、作用域链
面试题:
二、全局执行上下文代码的流程
三、执行上下文栈
预解析:
变量提升与函数提升
变量提升
函数提升
经典面试陷阱
暂时性死区(TDZ)
作用域:
一、什么是作用域
通常来说,一段程序代码中所用到的名字(变量名和函数名)并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突
js作用域(es6)之前:全局作用域 局部(函数)作用域,es6后有块级作用域
(1)、全局作用域
直接编写在script标签中或者一个单独的js文件内的Js代码
全局作用域在页面打开时创建,在页面关闭时销毁
在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建,我们可以直接使用它
es5中 在全局作用域中:创建的变量都会作为window对象的属性保存;创建的函数都会作为window对象的方法保存
全局作用域中的变量都是全局变量;在页面的任意部分都可以访问的到
var a=10;
var b=10;
// var c="hello";
console.log(window.c);//加window.,如果没有找到变量c,会undefined,不加的话,会报错
function fun(){
console.log("我是fun函数");
}
window.fun();//创建的函数都会作为window对象的方法保存
(2)、局部作用域(函数作用域)
在函数内部就是局部作用域。这个变量只在函数内部起作用
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
在函数作用域中可以访问到全局作用域的变量, 在全局作用域中无法访问到函数作用域的变量
es5的环境下,在函数中如果要访问全局变量,可以使用window对象
var a = 10;
function fun() {
var a = "我是fun函数中的变量a";
//console.log("a="+a);
function fun2() {
// console.log(a);
console.log("a=" + window.a);
}
fun2(); //10
}
fun();
(3)、作用域链
只要是代码,就至少有一个作用域
写在函数内部的局部作用域
如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
当在函数作用域中操作一个变量时,它会先在自身作用域中寻找,如果有,就直接使用,如果没有,就去它上一级作用域去寻找, 如果全局作用域中依然没找到,则会报错
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称做是作用域链
面试题:
//第一题
var x = 10;
function fn() {
console.log(x);//10
}
function show(f) {
var x = 20;
f();
}
show(fn);
//第二题
/*说说它们的输出情况*/
var fn = function () {
console.log(fn);//输出函数本身
};
fn(); //函数
var obj = {
fn2: function () {
// 在自己身上找,找不到fn2,然后去全局找,也找不到所以报错
console.log(fn2); //报错
// 通过this在自己身上找,找到了fn2
// console.log(this.fn2)//输出函数本身
},
};
obj.fn2();
二、全局执行上下文代码的流程
在执行全局代码前将window确定为全局执行上下文顶级对象
对全局数据进行预处理
var定义的全局变量==>undefined, 添加为window的属性
function声明的全局函数==>赋值(fun), 添加为window的方法
this==>赋值(window)
开始执行全局代码
三、执行上下文栈
1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
4. 在当前函数执行完后,将栈顶的对象移除(出栈)
5. 当所有的代码执行完后, 栈中只剩下window

预解析:
变量提升与函数提升
JS代码是由浏览器中的JS解析器来执行的,JS解析器在运行JS代码的时候分为两步:先预解析再代码执行
预解析:js引擎会把js里面所有的var 还有function提升到当前作用域的最前面
代码执行:按照代码书写的顺序从上往下执行
预解析分为变量预解析(变量提升)和函数预解析(函数提升)
变量提升 就是把所有var的变量声明提升到当前的作用域最前面,不提升赋值操作
函数提升 就是把所有的函数声明提升到当前作用域最前面,不调用函数
变量提升
为了理解变量提升,首先我们要将声明与赋值分开
当我们写var name="张三" ;时,JavaScript 引擎其实把这句话看成了两行代码:
-
var name;这是声明(告诉引擎有一个变量叫 name)
-
name="张三";这是赋值(把值放进去)
下面代码的输出结果是什么?
console.log(a);
var a = 10;
按照正常的逻辑(从上往下运行),第一行应该报错才对,因为它这时候还没遇到a的定义
但在浏览器里运行,它不会报错,而是输出 undefined
// --- 预解析阶段(偷偷做的事)---
var a; // 1. 把声明提到最上面,默认值是 undefined
// --- 执行阶段(真正运行的事)---
console.log(a); // 2. 此时 a 存在,但还没赋值,所以是 undefined
a = 10; // 3. 赋值留在了原地
结论:var的预解析只提升了名字,把值丢在了原地。所以它不报错,但也记不住值。
函数提升
函数声明的待遇比var高级得多。它享有整体提升的特权
sayHi(); // 我在定义之前就调用了!
function sayHi() {
console.log("你好!");
}
这段代码是可以正常运行的!输出 "你好!"
浏览器眼中的代码长这样:
// --- 预解析阶段 ---
function sayHi() { // 整块都被提上来了
console.log("你好!");
}
// --- 执行阶段 ---
sayHi(); // 所以这里能找到函数,正常执行
经典面试陷阱
这段代码是报错,还是输出我吃饱了?
eat();
var eat = function() {
console.log("我吃饱了");
};
报错,在在预解析(Hoisting)的眼里,变量声明和赋值是被强行拆散
// --- 1. 预解析阶段 ---
var eat; // 变量 eat 被提升了,但默认值是 undefined
// 注意:后面的 function... 并没有跟着上来!
// --- 2. 执行阶段 ---
eat(); // 此时 eat 的值是 undefined。
// 你试图运行 undefined(),JS 引擎就会怒吼:
// "TypeError: eat is not a function"
eat = function() { // 这一步才把函数赋值给 eat,但太晚了,程序上面已经挂了
console.log("我吃饱了");
};
暂时性死区(TDZ)
var与let和const的区别:以下代码会怎样?
console.log(name); // 这里会打印 undefined 吗?
let name = "张三";
会报错:Uncaught ReferenceError: Cannot access 'name' before initialization
(无法在初始化之前访问 'name')
为了搞懂var和let的区别,我们需要把“预解析”拆得更细一点。一个变量的诞生其实分三步:
-
创建 (Creation):在内存里登记名字“我有个变量叫 name”。
-
初始化 (Initialization):给它一个默认值(比如 undefined)。
-
赋值 (Assignment):真正把"张三"赋给它。
JS引擎在也会预解析对let进行创建,但不会初始化一个undefined。而是在外面加个锁(暂时性死区),在程序读到正式赋值那一行之前,谁”碰“谁“死”。
目的:ES6想要把let 设计得更安全、更规范(不像var 那样既污染全局window,又允许在声明前乱用),所以才引入了 TDZ(死区) 机制。





