预编译、变量提升、作用域
1. 预编译
JavaScript
编译过程只有下面三个步骤:
- 语法分析
- 预编译
- 解释执行
预编译分为全局预编译和函数预编译:
-
全局预编译发生在页面加载完成时执行
-
函数预编译发生在函数执行的前一刻
1-1 全局预编译
全局预编译步骤:
-
创建
GO
(Global Object
,全局执行期上下文,在浏览器中为window
)对象 -
找形参和变量声明,将变量声明和形参作为
GO
的属性名,并赋值为undefined
-
在全局里找函数
function
声明,将函数名作为GO
对象的属性名,并赋值为函数体 -
执行代码
console.log(global); // undefined
global = 100;
console.log(global); // 100
function fn() {
console.log(global); // undefined
global = 200;
console.log(global); // 200
var global = 300;
console.log(global); // 300
}
fn();
- 创建
GO
对象
GO{
// 空对象
}
- 找形参和变量声明,将变量声明和形参作为GO的属性名,值为
underfined
GO: {
global: undefined
}
- 在全局里找函数声明,将函数名作为
GO
对象的属性名,值赋予函数体
GO: {
global: undefined
fn: function() {}
}
**Tips:**这里函数声明会带来函数自己的 AO,预编译过程使用函数预编译步骤
1-2 函数预编译
函数预编译步骤:
- 创建
Activation Object
(以下简写为AO对象) - 找形参和变量声明,将变量声明和形参作为 AO 的属性名,值为
underfined
- 将实参和形参值统一,即更改形参后的
undefined
为具体的形参值 - 在函数体里找函数声明,将函数名作为 AO 对象的属性名,值赋予函数体
- 执行代码
// 函数预解析
function fn(arg) {
console.log(num); // undefined
console.log(fn1); // fun1() {console.log('函数声明')}
console.log(fn2); // undefined
console.log(fn3); // fun3() {console.log('函数声明')}
console.log(arg); // 1
var num = 123;
function fn1() {
console.log('函数声明')
};
var fn2 = function() {
console.log('函数表达式')
};
function fn3() {
console.log('函数声明')
}
}
fn(1);
- 创建AO对象
AO{
//空对象
}
- 找形参和变量声明,将变量声明和形参作为AO的属性名,值为
undefined
AO{
arg: undefind,
num: undefined,
fn2: undefined
}
- 将实参和形参值统一
AO{
arg: 1,
num: undefined,
fn2: undefined
}
- 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
AO{
arg: 1,
num: undefined,
fn2: undefined,
fn1: fun1() {console.log('函数声明')},
fn3: fun3() {console.log('函数声明')}
}
完整的预编译过程
AO{
arg: undefined -> 1,
num: undefined,
fn2: undefined,
fn1: fun1() {console.log('函数声明')},
fn3: fun3() {console.log('函数声明')}
}
2. 变量提升
2-1 什么是变量提升?
javascrript
代码执行前,浏览器会将 var
关键字的变量提前进行声明 ,并将该变量赋值为 undefined
function
关键字声明的函数,同样可以在声明前使用,其函数体也被提升到头部
这种预先处理的机制就叫做变量提升
// 变量提升并赋值为undefined
console.log('声明前:' + username); // undefined
var username = 'jsx';
console.log('声明后:' + username); // jsx
// 函数声明,函数名提升,并赋值函数体
console.log(getName('ljj')); // ljj
function getName(name) {
return name
};
console.log(getName('ljj')); // ljj
2-2 函数声明提升
函数声明:在主代码流中声明为单独的语句的函数
- 在函数声明被定义之前,它就可以被调用
// 函数声明
fn(); // 函数声明
function fn() {
console.log('函数声明')
}
函数表达式:在一个表达式中或另一个语法结构中创建的函数
- 函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用
// 函数表达式
console.log(fn1); // undefined
fn1(); // error: 因为var变量提升导致此时fn1赋值为undefined
// undefined() 报错
var fn1 = function() {
console.log('函数表达式')
}
console.log(fn2); // error 块级作用域中变量不会提升到顶部
let fn2 = function() {
console.log('函数表达式')
}
2-3 函数变量同名提升
在 var
和 function
同名的变量提升的条件下,函数会先执行
var 和 function
的变量同名 var
会先进行变量提升,在变量提升阶段,函数声明的变量会覆盖 var
的变量提升,所以直接结果总是函数先执行优先
// 同名情况下
// var先提升,然后函数名提升覆盖提升
console.log(username); // function username(){console.log('同名提升')}
var username = 'jsx';
function username() {
console.log('同名提升')
}
console.log(username); // jsx
console.log(username()); // 'jsx'() error
- 变量重名在变量提升阶段会重新定义也就是重新赋值
console.log('1',fn())
function fn(){
console.log(1)
}
console.log('2',fn())
function fn(){
console.log(2)
}
console.log('3',fn())
var fn = 'jsx'
console.log('4',fn())
function fn(){
console.log(3)
}
/* 输出
* 3
* 1 undefined
* 3
* 2 undefined
* 3
* 3 undefined
* Uncaught TypeError: fn is not a function
/
变量提升机制,fn
会多次执行,步骤如下:
- 同样由于变量提升机制,
fn
会被多次重新赋值最后赋值的地址为最后一个函数 - 调用
fn
都只是在调用最后一个函数输出都是3
, - 代码执行到
var fn = 'jsx'
,所以fn()
其实等于'jsx'()
导致类型错误TypeError
3. 作用域
3-1 什么是作用域?
作用域是代码在运行时,某些特定部分中的变量、函数和对象的可访问性,作用域决定了变量与函数的可访问范围
作用域的用处:隔离变量,不同作用域下同名变量不会有冲突
JavaScript
中的作用域主要分为:
- 全局作用域(Global Scope)
- 局部作用域(Local Scope)
// 全局作用域
var global = 'global';
globalFn(); // 函数可以在任何地方使用
function globalFn() {
// 变量可以在任何地方访问
console.log(global); // global
}
// 局部作用域
function partFn() {
var part = 'part';
function inPartFn() {
// 函数内部可以访问外部
console.log(part); // part
}
inPartFn();
}
partFn();
console.log(part); // error:外部无法访问内部变量
3-2 全局作用域
拥有全局作用域的变量或者函数可以在任何地方访问,也被称为全局函数、全局变量
在全局作用域中:
-
创建的变量都会作为
window
对象的属性保存 -
创建的函数都会作为
window
对象的方法保存
**Tips:**函数定义与变量定义都在全局作用域中会污染全局命名空间,容易引起命名冲突
- 最外层函数声明和最外层
var
变量声明
var global = 'global';
function globalFn() {
console.log(global); // global
function inpartFn() {
console.log(global); // global
}
inpartFn();
}
globalFn();
- 所有未声明定义直接赋值的变量
nodefind = '未声明'
console.log(nodefind); // 未声明
function Fn() {
console.log(nodefind); // 未声明
// 函数作用域定义未声明赋值变量
nodefindFn = '函数->未声明赋值变量'
console.log(nodefindFn); // // 函数->未声明赋值变量
}
Fn();
console.log(nodefindFn); // 函数->未声明赋值变量
- 所有
window
对象的属性
window.jsx = 'jsx';
console.log(window);
function attrFn() {
console.log(jsx);
}
attrFn(); // jsx
3-3 函数作用域
定义在函数中的变量就在函数作用域中,函数作用域也是局部作用域, var
关键字声明的为局部变量,在函数执行前会变量提升到当前作用域顶部
- 内层作用域可以访问外层作用域的变量,反之则不行
var outerLayer = '外层';
function outerFn() {
console.log(outerLayer); // 外层
var inLayer = '内层';
console.log(inLayer);
console.log(inEnd); // error: 内层不能访问外层
function inFn() {
console.log(inLayer); // 内层
var inEnd = 'inFn-> 内层'
}
inFn();
}
outerFn()
- 函数作用域中,没有
var
声明的关键字为全局变量,有var
关键字声明的为局部
function global() {
var sendVar = '使用var声明';
noSendVar = '未用var声明';
}
global();
// 外部可访问
console.log(noSendVar); // 未用var声明
console.log(send); // error: 外部无法访问
- 定义形参就相当于在函数作用域中声明了变量
// 形参也是局部变量,外部无法访问
function parms(arg) {
console.log(arg);
}
parms(); // undefined
parms(123); // 123
console.log(arg); // erro
3-4 块级作用域
在ES5
中, {}
大括号代码块,if
,switch
,for
,while
语句中不管条件是否成立 var
声明的变量会提升,外部可以访问,不存在块级作用域
在ES6
中,块级作用域可通过新增关键字 let
和 const
声明,所声明的变量在指定块的作用域外无法被访问
{
var ES5 = 'ES5';
}
console.log(ES5); // ES5
// ES6中通过let,const关键字声明的变量有块级作用域,外部无法访问
{
let ES6 = 'ES6';
console.log(ES6); // ES6
}
console.log(ES6); // error: 外部无法访问
- 声明变量不会提升到代码块顶部
// let, const声明变量不会提升
console.log(username, MYNAME); // error: 不会提升
let username = 'jsx';
const MYNAME = 'ljj';
- 不允许从外部访问块级作用域内部变量
{
let ES6 = 'ES6';
console.log(ES6); // ES6
}
console.log(ES6); // error: 外部无法访问
- 禁止重复声明
let againVar = '重复声明';
let againVar = '重复声明';
console.log(againVar); // error:不能重复声明
- 每一层都是独立作用域,里层作用域可以声明外层作用域同名变量,但不会改变外层变量
function run() {
value = "jsx";
if (true) {
let value = "ljj";
console.log(value); //ljj
}
console.log(value); //jsx
}
run();
3-5 作用域链
在 javascript
中使用一个变量或者函数时,Javascript
引擎会尝试在当前作用域下去寻找该变量或者函数:
- 变量或者函数在全局作用域时,则在全局作用域里找
- 变量或者函数在局部作用域时,函数可以无限嵌套产生不同的作用域,当前作用域内找不到就会向外层作用域一层一层找,采用就近原则,这种结构称之为作用域链
function outerFn() {
let username = 'jsx';
function outerFn1() {
function outerFn2() {
function outerFn3() {
console.log(username); // jsx
}
outerFn3();
}
outerFn2()
}
outerFn1()
}
outerFn();
3-6 静态作用域(词法作用域)
所谓静态作用域,其实就是指的词法作用域
- 在程序编译期通过对源代码的词法分析就可以确定某个标识符属于哪个作用域、作用域的嵌套关系(作用域链)
// foo 在全局定义,value会在全区作用域找
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
// foo 函数在函数作用域,value会向外层找
var value = 1;
function bar() {
var value = 2;
function foo() {
console.log(value);
}
foo();
}
bar(); // 2
3-7 静态与动态作用域
动态作用域是在运行时确定的,词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈的
动态作用域的作用域是基于调用栈的,而静态作用域的则是在定义的时候就确定了的,javascript
的词法作用域是静态作用域
js
语言中的变现为this
也就是上下文环境- 因为
this
是指向的是函数运行时所在的环境,也就是说只有到了执行时才能确定
let method = {
fn: function() {
console.log(this.name)
},
name: 'jsx'
}
var name = 'ljj'
var changeName = method.fn;
console.log(changeName);
// ƒ () {
// console.log(this.name); // this 指向window window.name为ljj
// }
method.fn(); // jsx this 指向对象method
changeName(); // ljj
4. var、let 和 const
4-1 var 关键字
var
关键字没有块级作用域var
全局声明的变量存在于window
对象中- 同一作用域下
var
关键字可以重复声明 var
关键字存在变量提升
// var 关键字可以重复声明
var message = 'jsx';
var message = 'jsx';
console.log(message); // jsx
// var全局声明变量在window对象中
var global = '全局';
console.log(window.global == global); // true
// var 没有块级作用域
if (true) {
var block = 'block'
console.log(block);
}
// 外部能访问{}块语句内变量
console.log(block); // block
// 变量提升
console.log(update); // undefined
var update = '变量提升'
function updateFn() {
console.log(inUpdate); // undefined
var inUpdate = '函数内变量'
}
updateFn();
4-2 let 关键字
-
let
声明拥有块级作用域 -
同一作用域下
let
定义变量不可以重复声明 -
let
关键字不存在变量提升 -
let
暂时性死区
// let关键字有块级作用域
{
let num = 100;
console.log(num); // 100
}
// 外部无法访问内部变量
// console.log(num); // error
// 同一作用域下不可重复声明
console.log(next); // 不存在变量提升
// 暂时性死区,得先声明再使用
// Cannot access 'next' before initialization
let next = 'next';
if (true) {
let next = 'next to';
console.log(next); // next to
}
console.log(next); // next
4-3 暂时性死区TDZ
TDZ
又称暂时性死区,指变量在作用域内已经存在,但必须在let/const
声明后才可以使用
TDZ
可以让程序保持先声明后使用的习惯,让程序更稳定
- 变量要先声明后使用
- 建议使用
let
/const
而少使用var
使用let
/const
声明的变量在声明前存在临时性死区(TDZ)使用会发生错误
// console.log(a); // Cannot access 'a' before initialization
let a = 1;
value = "jsx";
function getValue() {
console.log(value); // Cannot access 'hd' before initialization
let value = "ljj";
}
getValue();
let
和 const
声明的就会被放到TDZ中,前提是只会针对当前作用域内有效
// let和const声明的就会被放到TDZ中,前提是只会针对当前作用域内有效
console.log(typeof str) // "undefined"
if (true) {
console.log(str); // Cannot access 'str' before initialization
let str = "hello"
}
4-4 const 关键字
-
const
关键字定义常量,常量名建议全部大写 -
const
声明时必须同时赋值 -
const
不存在变量提升 -
const
声明拥有块级作用域 -
同一作用域下
const
定义变量不可以重复声明 -
可以修改引用类型变量的值,内存地址不能修改,但可以修改里面的属性
// 声明常量
// 不会变量提升,暂时性死区先声明后使用
console.log(MAXVALUE); // Cannot access 'MAXVALUE' before initialization
const MAXVALUE = 100;
console.log(MAXVALUE);
{
// 块级作用域
const TOPONE = 1;
console.log(TOPONE);
}
// 无法访问块作用域内部的变量
console.log(TOPONE); // TOPONE is not defined
// 声明时必须赋值
const LEFTNUM;
console.log(LEFTNUM); // Missing initializer in const declaration
// 同一作用域下const定义变量不可以重复声明
const TOPONE = 1000;
console.log(TOPONE); // 1000
// 可以修改引用类型的值
const obj = {
name: 'jsx',
age: 22
}
console.log(obj);
obj.name = '520->ljj';
console.log(obj); // {name: '520->ljj', age: 22}
// 同一作用域下,常量不可修改
const NOEDIT = 1;
NOEDIT = 2;
console.log(NOEDIT); // Assignment to constant variable