一、js的特点:
js是一门动态语言
、脚本语言
、解释型语言
、弱类型语言
。
二、什么是预编译:
js引擎会在解释JavaScript代码前先对其进行编译。
三、预编译的时机:
js运行时做三件事:
- 语法分析:对代码进行通篇检查,排除语法错误
- 预编译
- 解释执行:解释一行,执行一行
预编译分为全局预编译
和局部预编译
- 全局预编译在页面加载完后,检查没有语法错误,执行第一行代码前执行,产生GO对象
- 局部预编译在函数执行前的一瞬间执行,产生AO对象,函数执行完销毁AO。也就是说,函数执行两次,产出了两次AO,但这两个AO并无关联
四、预编译的步骤:
全局预编译
的步骤:
- 生成GO(global object)对象,这个GO就是window,是全局执行期上下文,默认有this、window、document三个属性
- 查找变量的声明,作为GO对象的属性名,值为undefined
- 查找函数的声明,作为GO对象的方法名,值为function
局部预编译
的步骤:
- 在函数执行前,为当前函数创建AO(activation object)对象,它是函数执行期上下文,在
调用结束
时销毁
,默认有this、arguments两个属性 - 查找形参和变量作为AO对象的属性名,值为undefined
- 使用实参的值改变形参的值
- 查找函数的声明作为AO对象的方法名,值为function
要注意:
①if判断中的var也会在预编译阶段进行变量提升,if语句到执行阶段才执行
if (x) {
var b = 1
}
②函数的AO是在函数执行的前一刻创建的,函数不执行就不会创建AO;但如果函数中使用了暗示全局变量,当函数执行的前一刻,该变量会被保存到GO中
function test() {
x = 100
}
test()
console.log(x) // 100
五、由预编译的过程推导优先级的问题
1、JavaScript中的变量名和函数名重名时,函数的优先级更高
a()
function a() {
console.log(b) // ƒ b() {}
var b
function b() {}
}
var a
console.log(a) // ƒ a() { ... }
为什么打印a和b时都是函数?
因为不管是全局预编译还是局部预编译,他们的步骤中最后一步是查找函数的声明作为GO或AO的方法名,此时如果在前面有同名的属性或方法,则会覆盖之前的属性或方法,最终的效果就是函数的提升会比变量的提升优先级高。
2、在函数内,优先级排序:局部函数 > 实参 > 形参/局部变量
1、形参和局部变量重名,以实参为准。因为实参赋值
的动作发生在查找形参和变量
之后
function a(i) {
console.log(i) // 100
var i = 1
}
a(100)
2、形参和局部函数重名,以局部函数为准。因为实参赋值
的动作发生在查找函数声明
之前
function b(i) {
console.log(i) // ƒ i() {}
function i() {}
}
b(100)
六、预编译时GO和AO的演变
示例1:
var a
function fn() {}
function a() {}
console.log(a)
var a = 100
console.log(a)
/*
全局预编译的过程:
1、生成GO对象
GO: {}
2、查找变量的声明,作为GO对象的属性名,值为undefined
GO: {
a: undefined
}
3、查找函数的声明,作为AO对象的方法名,值为function
GO: {
a: f a(){}, // 注意看,在预编译阶段a由undefined变为function了
fn: f fn(){}
}
至此,预编译结束,开始执行代码,当执行到 a = 100 时,GO对象为:
GO: {
a: 100,
fn: f fn(){}
}
所以,第一次打印a的位置,a为 f a(){},第二次打印a时,a为100
*/
示例2:
function fn(a) {
console.log(a)
var a = 123
console.log(a)
function a() {}
console.log(a)
var b = function bb() {}
console.log(b)
function c() {}
var c = a
console.log(c)
}
fn(1)
/*
局部预编译的过程:
1、生成AO对象
AO: {}
2、查找形参和变量作为AO对象的属性名,值为undefined
AO: {
a: undefined,
b: undefined,
c: undefined
}
3、使用实参的值改变形参的值
AO: {
a: 1, // 在此阶段,a的值由undefined变为1
b: undefined,
c: undefined
}
4、查找函数的声明作为AO对象的方法名,值为function
AO: {
a: f a(){}, // 在此阶段,a的值由1变为function
b: undefined,
c: f c(){} // 在此阶段,c的值由undefined变为function
}
至此,预编译结束,开始执行代码,第一次打印a,a为f a(){}。当执行到 a = 123 时,AO对象为:
AO: {
a: 123,
b: undefined,
c: f c(){}
}
第二次打印a时,a为123
声明函数a的这句代码在预编译阶段已执行,直接跳过,第三次打印a,还是123
执行到 b = function bb() {} 时,AO对象为:
AO: {
a: 123,
b: f bb() {},
c: f c(){}
}
所以打印b时,b为 f bb(){}
声明函数c的这句代码也是在预编译阶段执行过了,直接跳过,执行 c = a,所以打印c的值为123
*/
八、imply global暗示全局变量
什么叫暗示全局变量?
如果变量未经声明直接赋值,那么该变量为全局对象所有
function fn() {
var a = b = 1 // 等价于 var a = 1; b = 1
}
fn()
console.log(window.a)
console.log(window.b)
由此可知,var声明的变量,和暗示全局变量,都是在操作window,在window对象上添加了某个属性或方法。那加不加var的区别是啥?
①在全局作用域下,通过var声明的变量,是为window对象添加了一个不可配置(不可删除)
的属性;而不加var的变量,是为window对象添加了一个可配置(可删除)
的属性
var logo = 'volvo'
console.log(logo) // volvo
console.log(window.logo) // volvo
console.log('logo' in window) // volvo
delete window.logo // 这句代码无效
console.log(logo) // volvo
logo = 'volvo'
console.log(logo) // volvo
console.log(window.logo) // volvo
console.log('logo' in window) // volvo
delete window.logo // 这句代码无效
console.log(logo) // Uncaught ReferenceError: logo is not defined
②在局部作用域下,通过var定义的变量是该函数的局部变量
,并且会进行变量提升;没有通过var定义的变量,在进行读取和设置时都是在操作全局变量
,如果全局作用域下没有该变量,提前读取时会报错:Uncaught ReferenceError: logo is not defined
var logo = 'volvo'
function fn() {
console.log(logo) // undefined
var logo = 'bmw' // 通过var定义的变量会进行变量提升
}
fn()
var logo = 'volvo'
function fn() {
console.log(logo) // volvo 在当前作用域中找不到logo,就会向上查找,找到全局logo为volvo
logo = 'bmw' // 这是对全局变量重新赋值
}
fn()
console.log(logo) // bmw
③严格模式下,对未使用var声明的变量进行赋值会报错
'use strict'
logo = 'volvo' // Uncaught ReferenceError: logo is not defined
九、函数声明和函数表达式
函数声明:
function fn() {}
函数表达式:
var fn = function () {}
区别 | 函数名是否必须 | 调用时机 | 如何实现自执行 |
---|---|---|---|
函数声明 | 必须 | 函数提升(全局预编译已经将函数保存在GO中),可以在声明前调用 | function fn() { console.log('fn') }() 报语法错误,函数的声明和调用必须分开;可以使用() 将函数声明包裹起来变成表达式,或者在function前加上+/-/! ,然后可以在后面加上() ,这就是自执行函数 |
函数表达式 | 可选 | 变量提升,但是此时fn是undefined,必须要在函数表达式执行完才能调用 | const fn = function () { console.log('fn') }() 直接在表达式后面加上() 便可以自执行,要注意只有表达式才能被执行符号() 调用 |
建议:
由于js中存在函数提升,在遇到if语句、for循环、try/catch/finally语句时最好不要使用函数声明,推荐任何情况下都使用函数表达式