浏览器创建代码的过程:
/*
- 编译器-------------------把代码解析成浏览器懂得结构
-
词法解析 -
AST抽象语法树 -
构建出浏览器能能够执行的代码 - 编译器做完这些事之后交给了
-
↓ - 引擎(V8 webkit内核)
-
变量提升 -
作用域/闭包 -
变量对象 -
堆栈内存 -
GO/VO/AO/EC/EC Stack -
......
*/
JS引擎想要执行代码,一定会创建一个执行栈,也就是创建一个栈内存(EC Stack =>执行上下文环境栈)。
栈内存:提供环境,用来执行代码,并且可以存储基本类型的值。
EC:执行上下文,指的是当执行代码的时候,会形成一个全局的执行上下文(EC(G)global量),在里面也可以创建局部的执行上下文(EC(...),例如函数环境)。
注:某个域下的代码执行都有自己的执行上下文。
把压缩的上下文压缩到栈中执行 =>进栈
执行完有的上下文就没用了 =>出栈
有的还有用,会把其压缩到栈底 =>闭包
GO(全局对象):global object 在全局的执行上下文中,会有一个全局的对象,在浏览器端(不是node),这个对象有一些属性和方法(onload,setTimeout...)存放在了堆内存(Heap),然后又把全局对象赋值给window={xxx:xxx...},所以我们可以直接调用window.onload(),window.setTimeout()...,
我们还可以创建全局变量,并且给变量赋值
例1:
let a = 12 ;
let b = a;
b = 13;
console.log(a);
a = 12 变量赋值的三操作
创建变量(声明 declare)
创建值:基本值直接在栈中创建和存储即可
让变量和值关联起来(赋值) 也叫定义defined
所以当单独创建一个变量没有赋值的时候,是undefined
上面例子中
a指向了12,a————————12(指向也可以叫指针,所有编程语言都有指针的概念)
b指向了同一个12,
b又重新指向了13,此时b的指向变了,但是a没变,
所以a还是12
除了给变量赋值,我们还可以赋对象
例2:
let a = {n:12} ;
let b = a;
b['n'] = 13;
console.log(a.n);
由于引用值是复杂的结构,所以特殊处理-->开辟一个存储对象中键值对(或存储函数中代码)的内存空间,我们把这个空间叫堆内存,所有堆内存都有一个可被后续查找的16进制地址,而这个16进制地址是存放在栈内存当中的 后续关联赋值的时候,是把堆内存地址给予变量操作的,所以叫“引用值”
上面例子中
a指向了一个对象的16进制地址(假设为:AAAFFF000),
b指向了同一个对象的16进制地址,
b把该对象的"n"改成了13,
由于a和b指向的是同一个对象的地址
所以a.n也变成了13
再比如:
例3:
let a = {n:12} ;
let b = a;
b = {n:13} ;
console.log(a.n);
a指向了一个对象的16进制地址,
b指向了同一个对象的16进制地址,
b又指向了另一个对象的地址,此时a的指向没有变
所以a.n还是12
到此为止,只要a或者b的指向没有变,那么该对象就不会被销毁,如果想销毁,那么就改变a或者b的指向,那怎么样才能做到既把对象销毁了,又不会重新创建一个堆内存或者栈内存呢?
a = null;
b = null;
null代表空对象指针,是不会占用内存的,
null可以理解为意料之中,代表已知即将赋值,但是还没有赋值,可以先占个坑
undefined可以理解为意料之外,代表不知道会不会赋值
另:
例4:
let a = {n:12} ;
let b = a;
b.m = b = {n:20} ;
console.log(a);
console.log(b);
单赋值的时候a = {n:12},是从右往左执行
而b.m = b = {n:20},属性赋值比变量赋值优先级高,所以b.m赋值完b又重新赋值
所以a为{n:12,m:{n:20}},b为{n:20}
VO:在全局的执行上下文中,除了有赋值给window的全局对象以外,还会有我们自己创建的变量,我们把存储这个上下文当中的变量的地方,叫做"变量对象VO(G)"。
例5:
let x = [12,23] ;
function fn(y){
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
};
fn(x);
console.log(x);
不难看出,x存储在了我们上面说的VO(G)变量对象,x指向了一个数组,而数组也属于对象,所以该数组也占用了一个堆内存,数组内部与对象一样,也有很多键值对,其中把各个项的下标当做了键:
0:12
1::2
length:2
__proto__:.......
x存储在了VO(G)变量对象中,那fn呢?是不是也在其中呢?
答案是:是的!
函数也属于变量,和let和var创建的变量本质是一样的,区别是存储的值是个函数类型的值 创建函数的方式:
function fn(){}、let fn = function(){} 函数表达式
函数表达式的用处:
xxx.onclick = function(){}
xxx.addEventListener('click',function(){});
还有自调函数(function(){})()
~function(){}()
+/-/!function(){}()
但是带~/+/-/!的自调函数不可以自传参
数组里面有键值对,那函数呢?
创建函数的时候就定义了函数的作用域=>当前函数所在的上下文[[scope]]:EC(G)
由于函数也属于引用类型,所以该函数也占用了一个堆内存,其内部为
形参:y
函数内部的内容的代码字符串
"y[0]=100;y=[100]..."
也有对象有的存储键值对
length:1 形参个数
name:"fn"
prototype:...原型
...
AO:每一个函数执行都会形成一个全新的局部上下文,那此时在函数内部创建的变量,都会存在这个局部上下文中的变量对象,我们把它叫做"活动的变量对象(active object:AO)",可以理解为函数内部的变量对象,用来存储局部变量
由此我们可知,y为AO载体,
那么函数在执行内部代码之前,会先做几件事:
1.'初始化作用域链(scopeChain):<EC(fn)--EC(G)>'
2.初始化this指向:window
3.会有一个arguments,它是一个类数组,里面存放的实参的值或者16进制地址
arguments = {0:AAAFFF000}
4.会把形参y指向实参的值或者16进制地址
y = AAAFFF000
然后才代码开始执行
(1).因y指向了全局上下文中的x,所以改y[0]就是改x[0] = 100;
(2).把y又指向了另一个对象,那此时指针发生了改变,指向了[100]这个对象BBBFFF000,
(3).这时又把y[1]改变为200,此时改变的是BBBFFF000
(4).所以上面例子中结果为[100,200],[100,23]
由于fn函数上下文执行完毕后,里面的内容没有被占用,所以该函数出栈,被压在底部的EC(G)会弹到栈顶继续执行
fuinction fn(x,y){
/*
arguments = {0:10,1:20}
x = 10
y = 20
此时,非严格模式下,arguments会与形参建立映射机制
x = 10 -------> 0:10
y = 20 -------> 1:20
所以arguments里改了,形参的值也变了
*/
console.log(x,y,arguments) // 10,20,[10,20,caller...]
argument[0] = 100;
y = 200
console.log(x,y,arguments)// 100,200,[100,200,caller...]
}
fn(10,20);
如果是严格模式下
"use strict"
fuinction fn(x,y){
/*
arguments = {0:10,1:20}
x = 10
y = 20
此时,严格模式下,会阻断arguments与形参的映射机制
*/
console.log(x,y,arguments) // 10,20,[10,20,caller...]
argument[0] = 100;
y = 200
console.log(x,y,arguments)// 10,200,[100,20,caller...]
}
fn(10,20);
关于 逻辑与和逻辑或的优先级,以及在实际项目的几种写法:
例6:
var x = 10;
~function(x){
console.log(x);
x = x||20 && 30||40;
console.log(x)
}();
console.log(x);
自调函数也会有"创建+执行"的过程,只是我们平时把他们当成一步了
知识点:
A||B
A为真,返回A,否则返回B
A&&B
A为真,返回B,否则返回A
&&优先级高于||
使用过程中:
逻辑或:
//=>ES6中可以直接赋值初始值
function fn(x=0){};
//=>传统方式
function fn(x){
if(typeof x==='undefined'){
x = 0
}
或者
x = x || 0;
};
逻辑与:
typeof fn==='function'?fn():null;
或者
fn && fn(); //该写法不严谨,但是可以执行
所以上方代码结果为:30 10
谷歌浏览器和IE浏览器对"垃圾回收机制"处理的区别
例7:
let x = [1,2];
y = [3,4];
~function(x){
x.push('A');
x = x.slice(0);
//slice()会返回一个新数组,所以x会指向新数组的16进制地址,由于slice()里只有一个参数,所以截取到末尾,等于是重新克隆了一份给x
x,push('B');
x = y; //此时y是全局下的y
x.push('C');
console.log(x,y);
}(x);
console.log(x,y);
输出:[3,4,'C'], [3,4,'C']
[1,2,'A'], [3,4,'C']
此时,x,y被占用着,slice()新返回的数组没有被占用,所以跟着自调函数一起出栈
谷歌浏览器的"垃圾回收机制"(内存释放机制):
=>浏览器会在空闲的时候,把所有不被占用的堆内存,进行释放和销毁
IE浏览器的机制:
=>当前堆被占用一次,记数字1,再被占用一次,数字累加,当然取消占用,数字减去1,一直减到0则销毁