作用域
一个区域,在这个区域中声明的变量和函数只能在这个区域内部使用;
举个例子:
小明盖了一个牛场,那么牛场里面所有的牛在正常情况下只能在牛场里面活动,不能在牛场外面活动;那么这个牛场就相当于作用域,里面的牛相当于声明的函数和变量;当然在一定情况下牛是可以在牛场外面进行活动,比如你打开了门把牛放出去了,那么你打开了门相当于你在这个作用域中通过return等向外部暴露了变量,那么外部就可以使用这个变量了;
js中的作用域分为全局作用域,函数作用域和块级作用域;
函数在定义的时候,会在函数对象上创建一个[[scope]]属性,这个属性就是当前函数的作用域;
a[[scope]] = []
全局作用域
在一个js脚本中没有声明任何函数的时候此时的作用域就是全局作用,它是作用域中的最顶层的作用域;
举个例子
小明在未盖牛场前,他的牛在一片空地上,这片空地就相当于全局作用域,牛可以随意的奔跑
全局作用域的特点:
- 在全局作用域中定义的变量和函数,在全局中的任何地方都可以被使用
// 在全局作用域中定义一个变量
const a = 2
// 在全局作用域中定义一个函数
function b(){
// 在函数中使用全局作用域中定义的变量a
console.log(a) // 2
}
b()
- 在非严格模式下,未定义就使用的变量会被默认定义为全局变量,严格模式下会报错
function a(){
// 在函数内部给未定义的变量b赋值为2
b = 2
}
a()
// 在函数外部访问b
console.log(b) // 2
- 全局作用域中通过var关键字定义的变量都会被定义正window对象上,换句话就是window对象相当于全局对象,通过let和const关键字定义的变量不具有此特性
var a = 2
window.a // 2
const b = 3
window.b // undefined
全局作用域的缺点:
全局作用域中定义过多的变量和函数,容易造成命名冲突;
优点:
全局作用域中定义的变量和函数在全局任何地方都可以被访问;
函数作用域
函数作用域就是函数体这个范围,在函数内部定义的变量和函数,只能在函数内部使用,在函数外部是访问不到的;函数作用域在全局作用域的内部;函数作用域可以访问全局作用域中的变量,但是全局作用域无法访问函数作用域中定义的变量;
举个例子
和第一个例子一样,你盖的牛场相当于函数作用域,你的牛场内部的牛相当于在函数中定义的变量,正常情况下只能在函数内部使用,如果想在函数外部使用,那么需要return或window暴露出去供外部使用;
function a(){
// 函数内部定义一个变量b
var b = 1
// 函数内部声明一个变量
var c = 5
// 通过window暴露给外部使用
window.c = c
}
a()
// 在函数外部使用
console.log(b) // undefined
console.log(c) // 5
函数作用域的优点:
可以私有化变量,在函数内部定义的变量可以防止全局命名冲突和变量的污染;在jquery中就使用了函数作用域的特点;通过立即执行函数私有化变量,通过给window对象上添加$等属性暴露给外部使用;
块级作用域
块级作用域就是{}中let或const定义的变量只能在这个{}内部使用,因此{}对let或const就形成了块级作用域,通过var定义的变量不受块级作用域的限制;
// 在块级作用域中通过let定义的变量只能在块级作用域内部使用
for (let i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
console.log(i) // i is not defined
// var是不受块级作用域的限制
for (var i = 0; i < 5; i ++){
console.log(i) // 0 1 2 3 4
}
console.log(i) // 5
函数作用域和块级作用域的区别?
函数作用域中使用let或const定义变量,那么这个函数作用域也可看做是let或const的块级作用域;
块级作用域特点:
- 在块级作用域中通过let或const定义的变量不会进行变量提升,如果在定义前使用会形成暂时性死区;
function a(){
console.log(b) // Cannot access 'b' before initialization 暂时性死区
const b = 3
}
面试题:
// 下面代码输出什么?为什么?
for (var i = 0; i < 5; i ++){
setTimeout(()=>{
console.log(i) // ?
},0)
}
// 答案 会输出5个5,因为这里的var相当于在全局中定义的变量,0秒之后会在回调的箭头函数中查
// 是否有i这个变量,如果没有就在全局作用域找,此时的i已经是5了;
// 怎么才能按序输出?
// 1. 把var改成let,这样就形成了块级作用域,每次循环一次都会在这个块级作用域中定义一个i
// 并且赋值为当前的值;2. 通过函数传参的形式,这样就形成了函数作用域,会在函数作用域中查找;
// 3 通过立即执行函数传递参数的形式,也是利用了函数作用域
注意:
js中的作用域是静态作用域(词法作用域)也就是在定义的时候就确定了,而不是在执行的时候;
const a = 1
function b(){
console.log(a) //1 不是2
}
function c(){
const a = 2
b()
}
c()
虽然b函数在c中执行,c函数中也有变量a,但是作用域在函数b定义的时候就已经被确定了,所以在函数b中访问a的时候,会首先在b函数的作用域中查找,如果没有找到就向b函数所在的作用域中查找,这里也就是全局作用域,从全局作用域中找到了a=1,因此输出1
作用域链
一个个嵌套的作用域就形成了作用域链;全局作用域中声明了函数,那么全局作用域中就包含了函数作用域,这个就是作用域链;当访问一个变量的时候,首先会从它所在的作用域中查找,如果没有找到就会顺着作用域链一层层的向上查找,如果一直没有找到,就会一直查找到全局作用域;
const a = 1
function b(){
function c(){
console.log(a) // 1
}
c()
}
b()
c中访问了a属性,那么会在c的作用域中查找,如果没有找到就会在c函数所声明的地方b函数的作用域中查找,如果也没找到就会在b函数所声明的地方全局作用域查找找a=1;
面试题:
下面输出什么?
function a(fn, x){
if (x < 1) {
a(g, 1)
} else {
fn()
}
function g(){
console.log('x' + x)
}
}
function h(){
console.log('hh' + x)
}
a(h, 0)
答案:'x0' 注意变量的查找一定是根据作用域查找的,作用域一定是静态的也就是在声明的时候就确定了,因此x等于1的时候再执行fn,这个fn就是传递进来的g,g执行的时候输出x,g中没有x,就会在a的作用域中查找,此时的a作用域中的fn是h,x是0,因此输出'x0';
预编译
js运行时会进行三件事,语法分析,预编译,解释执行;预编译发生在函数执行前的那一刻;
语法分析:
判断代码有没有基本的书写语法错误;
解释执行:
一行一行执行代码;
预编译:
创建执行上下文;
执行上下文
函数每次执行都会创建一个执行上下文内部对象,一个执行上下文定义了函数执行时的环境,多次调用函数就会创建多个执行上下文,每个执行上下文都是独一无二的,创建好的执行上下文会被推入到执行栈中进行执行,当函数执行完毕,会弹出执行栈,执行上下文也会随之销毁;
执行上下文主要包含以下三个部分:
- VO(AO)变量对象
- this指向
- 作用域链
function a(){}
// a函数的执行上下文
ascopeContext = {
VO: {}, // 变量对象
this:null, // this
scope: null, // 作用域链
}
变量对象
存储函数内部定义的变量,形参和声明的函数;函数的变量对象叫AO;
创建变量对象的5个步骤:
- 创建AO对象;
- 根据形参初始化arguments对象;
- 查找定义的变量和形参,并作为AO的属性值为undefined;
- 把实参赋值为形参和arguments的值;
- 把函数式声明的函数作为AO的属性和值(重名的变量会进行替换,注意是函数式声明,表达式定义的函数不会进行第四步);
function a(){}
ascopeContext = {
AO: AO,
this:null,
scope: null,
}
举例说明
function a(c){
console.log(b) // undefined
var b = 2
console.log(b) // 2
console.log(d()) // 3
var d = 4
function d(){
return c
}
console.log(f) // v
var f = function(){}
}
a(3)
解析:
1. 函数a在执行前一刻进行预编译,创建执行上下文;
2. 创建了AO对象;
3. 根据形参初始化arguments,arguments: { 0: undefined, length: 1 }
3. 找到了变量b,d和形参c,并作为AO的属性赋值为undefined;
AO = { arguments: { 0: undefined, length: 1 }, b: undefined, d: undefined, f: undefined, c: undefined }
4. 把实参作为形参的值
AO = { arguments: { 0: 3, length: 1 }, b: undefined, d: undefined, f: undefined, c: 3 }
5. 把函数式声明作为AO的属性和值,(重名的变量会进行替换)
AO = { arguments: { 0: 3, length: 1 },b: undefined, c: 3, d: fn, f: undefined }
ascopeContext = {
AO: {
arguments: { 0: '3', length: 1 },
b: undefined,
c: 3,
d: fn,
f: undefined,
},
this:null,
scope: null,
}
注意:
全局环境下执行代码也会进行预编译,全局执行上下文会创建GO对象,和上面的步骤一样;
this指向
谁调用这个函数执行,那么this就指向谁,如果函数没有调用者,在非严格模式下this指向window,严格模式下是undefined;
function a(){}
a()
ascopeContext = {
VO: {},
this:window,
scope: null,
}
生成作用域链
当查找变量的时候会从当前函数的执行上下文中查找,如果没有找到就会沿着当前执行上下文的作用域链向上查找;
执行上下文中生成作用域链的步骤:
- 复制函数的作用域,添加到scope中;
- 把函数的变量对象添加到作用域链栈顶;
// a函数的作用域在定义的时候就确定了,其中包含了全局作用域 a[[scope]] = [window[[scope]]]
function a(){ var b = 1}
a()
ascopeContext = {
AO: {
arguments: { 0: 1, length:1}
b: 1
},
this:window,
scope: a[[scope]], // a[[scope]] = [VO, window[[scope]]]
}
举例说明:
var a = 1
funtion b () {
var c = 2
return c
}
b()
分析:
1. 创建全局作用域
window[[scope]] = []
2. 创建b函数的作用域,b函数的作用域中存放了它所在的作用域
b[[scope]] = [window[[scope]]]
3. 开始执行全局代码前进行预编译
3.1 创建全局执行上下文
GoscopeContext = {}
3.2 查找变量和函数声明,创建变量对象VO,同时修改window[[scope]] = [vo]
GoscopeContext = {
vo:{
a: undefined,
b: fn,
},
}
3.3 确定this
GoscopeContext = {
vo:{
a: undefined,
b: fn,
},
this: window,
}
3.4 生成作用域链
GoscopeContext = {
vo:{
a: undefined,
b: fn,
},
this: window,
scope: [vo,window[[scope]]]
}
3.5 执行代码,变量进行赋值,
window[[scope]] = [vo]
GoscopeContext = {
vo:{
a: 1,
b: fn,
},
this: window,
scope: window[[scope]]
}
4. 执行代码b()
4.1. b函数进行预编译
4.2. 创建b函数的执行上下文
bscopeContext = {
}
4.3. 给执行上下文添加环境变量,创建AO对象的四部,b[[scope]] = [AO,window[[scope]]]
bscopeContext = {
AO:{
arguments: { length:0}
c: undefined,
},
}
4.4 确定this指向
bscopeContext = {
AO:{
arguments: { length:0}
c: undefined,
},
this: window,
}
4.5 生成作用域链,添加b函数的作用域,并且添加AO对象到栈顶
bscopeContext = {
AO:{
arguments: { length:0}
c: undefined,
},
this: window,
scope: b[[scope]]
}
4.6 执行c=2,修改vo中的c为2
8. 执行retrun c,从bscopeContext中的vo中查找c,找到就返回没有找到就继续从b[[scope]]进行查找;
闭包
个人理解一个函数a内部返回了另一个函数b,并且b函数内部使用了a函数的变量,比过去b函数在外部被执行了,导致b函数使用的a函数内部的变量无法被释放,这个就是闭包
总结
作用域在函数定义的时候就已经确定了,执行上下文是在函数执行的时候创建的,执行上下文是创建了函数的执行环境;函数的每次执行都会创建一个新的执行上下文推入到执行栈中,函数执行完毕,推出执行栈;