先看几个名词解释:
- Execution Context Stack:执行环境栈,是浏览器执行执行JS代码的时候,需要提供的一个供代码执行的环境,是栈内存。我们简记为ECStack,
- Execution Context:执行上下文,是代码执行中,为了区分全局和函数执行所处的不同的作用域而开辟的内存。简记为EC,其中global全局上下文记作EC(G),函数执行的私有上下文记作EC([函数名/自定义])。相应代码执行的时候会按顺序进栈执行,执行完毕后出栈。注意,全局上下文的出栈只有页面关闭的时候才会进行。
- Variable Object和Active Object:变量对象,在每一个上下文代码执行的时候,都可能会创建变量,在每一个上下文中(不论是全局的还是私有的),都会有一个存储变量的空间,称为变量对象。存放当前全局上下文中变量的记作VO(G),存放当前私有上下文中变量的记作AO([自定义])。
- Gloable Object:全局对象,跟VO/AO没关系,浏览器把所有后期需要供JS调取使用的属性和方法(内置)都放置在GO对象中,并且在全局中创建一个叫做window变量指向它。GO所开辟的是一个堆内存。
举例说明
let a = 12;
let b = 13;
function func() {
let a = 1;
let b = 2;
}
func();
console.log(a, b);
/*
全局下的代码和函数中的代码,在执行的时候,为了区分代码执行所处的环境【即词法作用域--或者说目的是为了区分每个词法作用域下代码的独立性】,从而有了“执行上下文”的概念。
EC-执行上下文,代码执行所在的词法作用域,或者代码执行所处的范围。
形成执行上下文之后会进栈(执行环境栈)执行。
简单来说:
首先是创建EC(G),然后EC(G)进栈,
然后函数执行,创建出来一个函数执行的私有上下文EC(fun),进栈执行,
执行完毕EC(fun)出栈。而EC(G)的出栈是在页面关闭时候进行。
而在每一个上下文代码执行的时候,都可能会创建变量。
所以在每一个上下文中(无论EC(G)还是私有EC)都会有一个存储变量的空间VO / AO。
*/
堆栈内存中需要说明的一点是,基本类型值直接存储在栈内存中,而引用数据类型值需要先开辟一个堆内存,把相应的数据存储进去之后,再把堆内存的地址值存放到栈中,供变量调用。
//example1
var a = 12;
var b = a;
b = 13;
console.log(a);
对于上述这段简单代码的执行过程,如图所示,首先会开辟一个执行环境栈ECStack,然后会创建EC(G),在EC(G)中会有相应的数据,这些数据是全局上下文中的,属于VO(G)。然后将EC(G)进栈执行,具体是,创建一个变量存储在变量对象中,然后让变量对象和值进行关联(指针指向的过程).
创建变量且赋值的过程:1.创建一个值,基本类型值直接存储在栈内存中,而引用数据类型值需要先开辟一个堆内存,把内存存储进去后,把堆内存地址存放到栈中供变量调用;2.创建一个变量存储在变量对象中;3.让变量和值进行关联(指针指向的过程)。
//example2
var a = {n: 12};
var b = a;
b['n'] = 13;
console.log(a.n);
对于变量的值是对象/函数,都要先开辟堆内存(16进制地址)。而对象的成员访问,需要先基于16进制地址找到对应的堆内存,然后再把堆内存中的某个成员进行相应的操作。
//example3
var a = {n: 12};
var b = a;
b = {n: 13};
console.log(a.n);
关于对象中的属性名
对象数据类型,是由零到多组键值对(属性名和属性值)组成的。其中对于属性名的类型,存在两种说法: 【说法一】属性名类型只能是字符串或者Symbol; 【说法二】属性名类型可以是任何基本类型值,处理中可以和字符串互通。 无论哪一种说法,属性名绝对不能是引用类型,如果设置引用类型,最后也是转换为字符串处理的。我们知道数组是对象类型的,而数组中是以数字作为索引的,索引就是属性名。
let obj = {
0: 12,
true: 'xxx',
null:20
};
console.log(obj);//{0:12, true:"xxx", null:20}
console.log(obj[0]);//12
console.log(obj['0']);//12
console.log(obj[true]);//"xxx"
console.log(obj['ture']);"xxx"
let sy = Symbol('AA');
let x = {0: 0};
let obj = {
0: 12,
true: 'xxx',
null:20
};
obj[sy] = '好好学习';//如果写成obj.sy = "好好学习",那么输出结果中的属性名就是sy了
console.log(obj);//{0:12, true:"xxx", null:20, Symbol(AA):"好好学习"}
//console.log(obj["Symbol(AA)"]);//这个结果是undefined,是获取不到属性值的
console.log(obj[sy]);//“好好学习” 这个方式是可以获取到属性值的
obj[x] = 100;
console.log(obj);//{0: 12, true: "xxx", null: 20, [object Object]: 100, Symbol(AA): "好好学习"} 对于x对应的项,属性名并不是一个对象,而是[object Object],也就是浏览器会把对象变为字符串作为属性名
//for-in 遍历中获取的属性名(key)都会变为字符串
//并且无法迭代到属性名是Symbol类型的属性
for (let key in obj) {
console.log(key, typeof key);
/*
0 string
true string
null string
[object Object] string
*/
}
关于obj[x]和obj['x']的区别
let x = 20;
let obj = {
x: 100
};
//属性名肯定是一个值
console.log(obj[x]);//x是一个变量,并不是一个值,把x变量存储的值当作属性名,获取对象的属性值,也就是obj[20],但是obj中并没有属性名为20的属性,所以结果是undefined
console.log(obj['x']);//字符串‘x’是一个值,也就是获取属性名为x的属性值,所以结果是100。obj.x与这个写法是等价的
//输出结果
var a = {}, b = '0', c = 0;
a[b] = '好好学习';//a['0']='好好学习'
a[c] = '天天向上';//a[0]='天天向上'
console.log(a[b]);//=>"天天向上"
var a = {},
b = Symbol('1'),
c = Symbol('1');
a[b] = '好好学习';
a[c] = '天天向上';
console.log(a[b]);//结果是"好好学习"
//因为b和c是两个不同的唯一值,给a加个唯一属性,再给a加个唯一属性
var a = {},
b = {n: '1'},
c = {m: '2'};
a[b] = '好好学习';//a['[object Object]'] = '好好学习'
a[c] = '天天向上';//a['[object Object]'] = '天天向上'
console.log(a[b]);//'天天向上'
两道练习题
题目1.
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x);
console.log(b);
/*
补充知识:
连等赋值:
let a = b = 12;
1. 创建一个值12
2. b = 12//在window上映射一个b
3. let a = 12
运算符优先级:
赋值的优先级3,成员访问的优先级是19
a.x = a ={};
a = a.x = {};
由于成员访问a.x的优先级是很大的,所以不论怎么调换位置,都是先处理a.x = {}
解析:
1. 浏览器执行JS代码,提供一个执行环境栈ECStack,并将全局执行上下文EC(G)入栈,每一个上下文中都由一个存储变量对象的空间VO,在全局下是VO(G)
2. 创建一个堆内存,地址值记为AAAFFF000,其中存储属性名为n,属性值为1的对象
3. 将堆内存的地址值放入VO(G)中,并在VO(G)中创建一个变量a,将a和AAAFFF000关联在一起,也即是将AAAFFF000赋值给a
4. 创建一个变量b,也和AAAFFF000关联在一起
5. a.x = a = {n: 2}; 首先是创建值,新开辟一个堆内存AAAFFF111,里面存储{n: 2},然后将地址值放入VO(G),然后根据运算符的优先级,先让a.x = AAAFFF111,再让a = AAAFFF111,而对于a.x怎么找到?需要根据AAAFFF000找到对应的堆空间,发现其中没有x的属性,就会自行创建一个x属性,然后把AAAFFF111作为x的属性值存储在AAAFFF000中。以后就会通过堆内存AAAFFF000中的x指向堆内存AAAFFF111。接下来将VO(G)中的a变量的值变为AAAFFF111
6.经过以上一系列操作,
a = {n: 2};
b = {
n: 1,
x: {
n: 2
}
}
结果:
undefined
{n: 1, x: {…}}//AAAFFF000对象
*/
运算符的优先级汇总表
题目2.
var x = [12, 23];
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
fn(x);
//fn(x);//函数第二次执行,会形成一个全新的私有上下文,把之前做过的事情,还原封不动的再执行一次(所有的东西都是从头来一遍的),此时形成的上下文和上一次形成的上下文之间没有必然的联系。
console.log(x);
/*
结果:
[100, 200]
[100, 23]
*/
解析:
创建函数:
- 开辟一个堆内存(16进制的内存地址,AAAFFF111)
- 声明当前函数的作用域(在哪个上下文中创建的,它的作用域就是谁)。本题中作用域是全局下的,即【[scope]】:EC(G)。【我们可以顺便标记一下形参(方便我们后期执行的时候看看有没有形参)】
- 把函数体中的代码当作“字符串”存储在堆内存中(创建一个函数,存储的是一堆字符串,所以函数只要不执行,函数其实没啥意义)
- 把函数堆内存的地址,类似于对象一样,放置在栈中供变量(函数名)调用。 【注意 var fn=xxx和function fn(){},两个fn都是全局变量,而且是同一个变量,只是赋值不一样(function fn(){}相当于声明一个变量,变量存储的值是函数而已)。function fn(){}和var fn=function(){}意义是一样的,区别只是变量提升】
执行函数:
- 会形成一个全新的私有上下文EC(xx)(目的是供函数体中的代码执行),然后进栈执行
- 在私有上下文中有一个存放私有变量的变量对象,即AO(xx)
- 在代码执行之前要做的事情很多:
- 初始化它的作用域链<自己的上下文,函数的作用域>
- 初始化this
- 初始化arguments实参集合(箭头函数没有arguments)
- 形参赋值(形参变量是函数的私有变量,需要存储在AO中的)
- 变量提升(在私有上下文中声明的变量都是私有变量)
- ......
- 代码执行(把之前在函数堆中存储的字符串,拿过来在上下文中依次执行)
- (形参变量是私有的变量,本题中y是私有的)
- 作用域链查找机制:在代码执行中,遇到一个变量,我们首先看一下是否为自己的私有变量,如果是自己的私有变量,接下来的所有操作都是私有的(和外界没有直接联系);如果不是自己私有的,则按照scope-chain,向上级上下文中查找(如果是上级私有的,接下来的操作都是操作上级上下文中的变量)...一直找,直到找到EC(G)为止
- 根据实际的情况确定当前上下文是否出栈释放
- 为了保证栈内存的大小(内存优化),一般情况下,如果当前函数执行产生的上下文,在进栈且代码执行完成后,会把此上下文移出栈(上下文释放了:之前在上下文中存储的私有的变量等信息也就跟着释放了)。全局上下文是在打开页面生成的,也需要在关闭页面的时候释放掉(只有页面关闭才会释放掉)。刷新页面是把原有的上下文释放,再重新创建新的上下文。
- 特殊情况:只要当前上下文中的某些内容,被上下文以外的东西占用,那么当前上下文是不能被释放的(上下文中存储的变量等信息也保留下来了)。这种情况就是大家认为的闭包。
- 如果函数第二次执行,会形成一个全新的私有上下文,把之前做过的事情,还是原封不动的再执行一次(所有的东西都是从头来一遍的),此时形成的上下文和上一次形成的上下文之间没有必然的联系。