数据渲染机制及堆栈内存
栈内存:提供代码执行的环境
堆内存:存放东西(存放的是属性和方法)
浏览器会在计算机内存中分配一块内存,专门用来供代码执行的。
=》栈内存ECStack,执行环境栈
全局对象GO。浏览器会让window指向GO,浏览器把内置的一些方法和属性放在一个单独的内存中 ,叫堆内存(heap)
任何开辟的内存都有一个16进制的内存地址,方便后期找到这个内存(isNaN parseInt)
先进后出,垃圾回收机制
VO(变量对象):在当前的上下文中,用来存放创建的变量和值得地方(每一个执行上下文中都会有一个自己的变量对象)。函数私有上下文中叫做AO(活动对象,但是也是变量对象,AO是VO的分支。)
var a = 12;
创建变量和赋值操作的底层操作有3部:
- 创建一个值
- 创建一个变量
- 让变量和值关联
基本数据类型值都是直接存在栈内存中。而引用类型值是先开辟一个堆内存,把地址存储进去,最后把地址放到栈中供变量关联使用的。
所有的指针赋值都是指针的关联指向。
对象:
- 创建一个堆内存
- 把键值对存储到堆内存中
- 堆内存地址放到栈中,供变量调用
- 栈内存也是作用域(包含全局栈内存 和 私有栈内存)
- 提供一个供JS代码自上而下执行的环境(代码都是在栈中执行的)
- 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的
=>当 栈内存被销毁,存储的那些基本值也都跟着销毁了。
- 堆内存:引用值对应的空间
- 存储引用类型值得(对象:键值对 函数:代码字符串)
=> 当前堆内存释放销毁,那么这个引用值彻底没了
=> 堆内存的释放:当堆内存没有被任何的变量或者其他东西所占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁掉(谷歌浏览器)
xxx=null 通过空对象指针null可以让原始变量(或者其他东西)谁都不指向,那么原有被占用的堆内存就没有被东西占用了,浏览器会销毁它。
考察 创建变量和赋值操作的底层操作有3步
1. 创建值
+ 开辟一个堆AAAFFF000
+ 存储键值对
name:'hhy'
fn:自执行函数执行,需要把obj.name的值当做实参传递进来 =>其实是obj找不到,因为这个时候还没赋值 undefined.name
var obj = {
name: 'hhy',
fn: (function(x){
return x + 10;
})(obj.name)//在给fn赋值的时候,是把自执行函数执行的返回结果赋值给fn属性
}
console.log(obj.fn) // 报错name找不到
复制代码
学会画框框,分析变量提升和栈堆内存,才能出正确的结果
变量提升
当栈内存(作用域)形成,JS代码自上而执行之前,浏览器首先会把所有带var、function关键词的进行提前“声明”或者“定义”,这种预先处理机制称之为“变量提升”。
- 声明(declare):var a (默认值undefined)
- 定义(defined):a=12 (定义其实就是赋值操作)
变量提升阶段
-
带var的只声明未定义
-
带function的声明和赋值都完成了
-
变量提升只发生在当前作用域
例如:开始加载页面的时候只对全局作用域下的进行提升,因为此时函数中存储的都是字符串而已
- 在全局作用域下声明的函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变量”
带var和funciton的才是声明
-
浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
-
私有作用域形成后,也不是立即代码执行,而是先进行遍历提升(变量提升钱,先形参赋值)
-
在ES3/ES5语法规范中,只有全局作用域和函数执行的私有作用域(栈内存),其它大括号不会形成栈内存
带var和不带的区别
在全局作用域下声明一个变量,也相当于给window全局对象设置了一个属性,变量的值就是属性值(私有作用域中声明的私有变量和window没啥关系)
console.log(a) // undefined
console.log(window.a) //undefined
console.log('a' in window) // true
在变量提升阶段,在全局作用域中声明了一个变量A,此时就已经把A当做属性赋值给window了,只不过此时还没有给A赋值,默认值undefined
in:检测某个属性是否隶属于这个对象
复制代码
**全局变量和window中的属性存在“映射机制” **
console.log(a) // 报错
console.log(window.a) //undefined
console.log('a' in window) //false
a = 12; //window.a = 12
console.log(a) // 12
console.log(window.a) // 12
复制代码
不加var的本质是window的属性
var a = b = 12; 这样写的b是不带var的
相当于
var a = 12;
b = 12;
复制代码
console.log(a,b) //undefined undefined
var a = 12, b =12;
function fn(){
console.log(a,b) //undefined 12
var a = b = 13;
console.log(a,b)//13 13
}
fn()
console.log(a,b); //12 13
复制代码
私有作用域中带var和不带也有区别:
- 带var的在私有作用域变量提升阶段,都声明为私有变量,和外界没有任何的关系
- 不带var不是私有变量,会向它的上级作用域查找,看是否为上级的变量,不是,继续向上查找,一直找到window为止(我们把这种查找机制叫做:作用域链),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的
变量提升的一些细节
只对等号左边进行变量提升
在当前作用域下,不管条件是否成立成立都要进行变量提升
- 带var的还是只声明
- 带function的在老版本浏览器渲染机制下,声明和定义都处理,但是为了迎合es6中的块级作用域,新版浏览器对于函数(在条件判断中的函数),不管条件是否成立,都只是先声明,没有定义,类似于var
console.log(a) //undefined
if('a' in window){
var a = 100
}
console.log(a) //100
复制代码
f = function () {return true} ; => window.f=...
g = function () {return false}; =>window.g=...
//自执行函数肯定有私有作用域
~function (){
//变量提升:function g; //=>g是私有变量
if(g() && [] == ![]){ //新版本报错 g is not a function(此时g是undefined)
f = function() {return false} =>把全局中的d进行修改
function g() {return true} => 私有
}
}()
console.log(f())
console.log(g()) //执行全局的
//老版本if会执行,输出false false
复制代码
条件判断下的变量提升到底有多坑
console.log(fn) //undefined 变量提升
if(1 === 1){
console.log(fn)
//函数本身
function fn(){
console.log('ok')
}
}
console.log(fn) //函数本身
复制代码
当条件成立,进入到判断体中(在ES6中它是一个块级作用域)第一件事并不是代码执行,而是类似于变量提升一样,先把fn声明和定义了,也就是判断体中代码执行之前,fn就已经赋值了。
变量提升机制下重名的处理
-
带var 和 function 关键字声明相同的名字,这种也算是重名了(其实是一个fn,只是存储值得类型不一样)
-
关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】
fn() //4
function fn(){console.log(1);}
fn() //4
function fn(){console.log(2);}
fn() //4
var fn=100; //带var提升只是声明,没有赋值,代码执行的时候赋值
fn(); //报错,fn is not a function ,报错了后面的不会执行
function fn(){console.log(3);}
fn() //这里执行不到
function fn(){console.log(4);}
fn函数声明并赋值
复制代码
ES6的let不存在变量提升
在es6中基于let、const等方式创建变量或者函数,不存在变量提升机制。
- 切断了全局变量和window属性的映射关系
在相同的作用域中,基于let不能声明相同名字的变量(不管用什么方式在当前作用域下声明了变量,再次使用let创建都会报错)
虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测(语法检测);自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会在执行了(虽然没有把变量提前声明定义,但是浏览器已经记住了,当前作用域下有哪些变量)
a = 12;//报错:a has already been declared
console.log(a);
let a = 13;
console.log(a);
复制代码
var a = 12;//报错:a has already been declared
console.log(a);
let a = 13;
console.log(a);
复制代码
let a = 10, b = 10;
let fn = function(){
//私有作用域
console.log(a,b) //报错:a is not defined
let a = b = 20;
}
fn();
console.log(a,b)
复制代码
ES6的暂时性死区问题
var a= 12;
if(true){
//块级作用域
console.log(a) //报错:a is not defined
let a = 13;
}
复制代码
基于let创建变量,会把大部分{}当做一个私有的块级作用域(类似于函数的私有作用域),在这里也是重新检测语法规范,看一下是否是基于新语法创建的变量,如果是按照新语法规范来解析。
暂存死区
console.log(typeof a) // undefined
console.log(typeof a) // 报错:a is not defined
复制代码
在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明过的变量,不会报错,返回undefined
console.log(typeof a)// 报错:a is not defined
let a;
复制代码
如果当前变量是基于es6语法处理,在没有声明这个变量的时候,使用typeof检测会直接报错,不会是undefined,解决了原有的JS死区。
私有变量和全局变量
var a = 12, b = 13, c = 14;
function fn(a) {
console.log(a,b,c); //12 undefined 14
var b = c = a = 20;
console.log(a,b,c) //20 20 20
}
fn(a)
console.log(a,b,c) //12 13 20
复制代码
有关私有作用域和作用域链的练习
var ary = [12, 23];
function fn(ary){
// 形参ary是私有变量,和全局ary指向的地址是一样的
console.log(ary);
ary[0] = 100;
ary = [100]; // 重新指向新的地址,此处ary是私有的,跟全局地址没关系了
ary[0] = 0;
console.log(ary) // [0]
}
fn(ary)
console.log(ary) // [100, 23]
复制代码
注意:形参是私有变量
上级作用域查找
当前函数执行,形参衣蛾私有作用域A,A的上级作用域是谁,和他在哪执行的没有关系,和他在哪创建有关系,在哪里创建的,它的上级作用域就是谁。
var a = 12;
function fn() {
console.log(a) //12
}
function sum() {
var a = 120;
fn()
}
sum()
复制代码
var n = 10;
function fn() {
var n = 20;
function f() {
n++;
console..log(n);
}
f();
return f;
}
var x = fn(); // 第一次结果是21
x(); // 第一次结果是22
x(); // 第一次结果是23
console.log(n)//10
复制代码
闭包及堆内存释放
堆内存:存储引用数据类型值(对象:键值对 函数:代码字符串)
栈内存:提供JS代码执行的环境和存储基本类型值
- 【堆内存释放】
让所有引用堆内存空间地址的变量赋值为null即可(没有变量占用这个对内存了,浏览器会在空闲的时候把它释放掉)
- 【栈内存释放】
一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:
- 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面找不到原有的内容了)
- 全局栈内存只有在页面关闭的时候才会被释放掉
var i = 1;
function fn(i){
return function (n) {
console.log(n + (++i));
}
}
var f = fn(2) // i = 2
f(3) // n=3 6
fn(5)(6) //i=5 n=6 12
fn(7)(8) //i=7 n=8 16
f(4) // i=3 n=4 8
复制代码
var k = 1;
console.log(5 + k++, k) //6 2
k = 1
console.log(5 + ++k, k) //7 2
复制代码
闭包作用之保护
函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为“闭包”
市面上的开发者认为的闭包是:形成一个不销毁的私有作用域(私有栈内存)才是闭包
闭包:柯里化函数
function fn() {
return function () {}
}
// 被f占有了,不能被销毁
var f = fn();
复制代码
闭包:惰性函数 立即执行函数
var utils = (function(){
return {}
})()
复制代码
面试的时候可以写以上形式,都是形成不销毁内存
闭包项目实战应用:
真实项目中为了保证JS的性能(堆栈内存的性能优化),应该尽可能的减少闭包的使用(不销毁的堆栈内存是耗性能的)
- 闭包具有“保护”作用:保护私有变量不受外界的干扰
在真实项目中,尤其是团队协作开发的时候,应当尽可能得减少全局变量的使用,以防止相互之间的冲突(“全局变量污染”),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量
(function(){
var n = 12;
function fn(){console.log(n)}
return fn
})()
复制代码
不仅如此,我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用,这样我们如何处理呢?
- Jquery这种方式:把需要暴露的方法抛给全局
(function(){
function jQuery(){c}
window.jQuery = window.$ = jQuery; // 把需要供外面使用的方法,通过给win设置属性的方式暴露出去
})()
jQuery();
$();
复制代码
- Zepto这种凡是:基于return把需要供外面使用的方法暴露出来
var Zepto = (function(){
return {
xxx: function(){}
}
})()
Zepto.xxx()
复制代码
- 闭包具有“保存”作用:形成不销毁的栈内存,把一些值保存下来,方便后面得调取使用
闭包作用之保存
实例:tab切换
i不是私有的
for(var i=0, i<tabList.length;i++){
tabList[i].onclick = function() {
changeTab(i)//执行方法,形成一个私有的栈内存,遇到变量i,i不是私有变量,向上一级作用域查找(上级作用域是window),因为执行完了代码才会click,这时候全局i为3
}
}
以上点击的时候会报错
复制代码
在老版本中,判断和循环不能形成作用域,只有全局作用域和函数中的私有作用域
所有的事件绑定都是异步编程,所以当我们点击执行方法的时候,循环早已结束(让全局的i等于循环最后的结果3)
作用域查找机制 和 异步编程解释以上代码报错
解决方案:自定义属性
for(var i=0, i<tabList.length;i++){
tabList[i].myIndex = i
//点击的时候执行的是小函数,自执行函数在给时间赋值的时候就已经执行
tabList[i].onclick = function() {
changeTab(this.myIndex)//this指向当前操作的元素对象
}
}
复制代码
解决方案:闭包
for(var i=0, i<tabList.length;i++){
tabList[i].onclick = (function(n) {
var i = n;
return function () {
changeTab(i)
}
})(i)
}
复制代码
总结:循环3次,形成3个不销毁的私有作用域(自执行函数执行),而每一个不销毁的栈内存中都存储了一个私有变量i,而这个值分别是每一次执行传递进来的全局i的值(也就是:第一个不销毁额作用域存储的是0,第二个是1,第三个是2);当点击的时候,执行返回的小函数,遇到变量i,向它自己的上级作用域查找,找到的i值分别是:0/1/2,达到了我们想要的效果;
耗性能,虽然能实现,最好不要这么写
每个循序形成自己的私有作用域
for(var i=0, i<tabList.length;i++){
(function(n) {
tabList[n].onclick = function() {
changeTab(n)//this指向当前操作的元素对象
}
})(i)
}
复制代码
原理都是形成3个不销毁的私有作用域,分别存储需要的索引值
解决方案:基于es6
for(let i=0, i<tabList.length;i++){
tabList[i].onclick = function() {
changeTab(i)
}
}
复制代码
js执行上下文与作用域
- var和function声明创建在全局对象中,而let const class声明的变量创建在全局scope中
- 先在全局scope中找变量,查找不到再在全局对象查找
顶级函数:不在大括号中
全局上下文
Step1::创建全局执行上下文,并加入栈顶
Step2:分析:
- 找到所有的非函数中的var声明
- 找到所有的顶级函数声明
- 找到顶级let const class声明
Step3:
- 名字重复处理
Step4: 创建绑定
- 登录并初始化var为undefined
- 顶级函数声明:登录function名字,并初始化为新创建函数对象
- 块级中函数声明:登记名字,初始化为undefined
- 登记let cosnt class,但未初始化
Step5:执行语句
函数对象体内会保存,函数创建时的执行上下文的文本环境。它对理解函数闭包和函数执行作用域很有帮助。
函数的调用是创建函数执行上下文。比如foo(),是创建foo执行上下文。
作用域
- 作用域是解析(查找)变量名的一个集合,就是当前运行上下文(也可以是当前上下文的词法环境)
全局作用域就是全局运行上下文
函数作用域就是函数运行上下文
- 函数调用时的执行上下文看“身世”——函数在哪里创建,就保存哪里的运行上下文
函数的作用域是在函数创建的时候绝对的而不是调用的时候决定
词法作用域
- 并非根据调用嵌套形成(运行上下文)作用域,而是根据函数创建嵌套形成作用域链,也就是
函数的书写位置形成作用域链
,因此称为词法作用域。
function foo(){
console.log(a) // 2
}
function bar(){
var a = 3;
foo();
}
var a = 2;
bar()
复制代码
块级作用域
let a = 'out if statement'
if(true){
let a = 'in if statement'
console.log(a)
}
console.log(a)
复制代码
总结关键点
- 在当前作用域下,不管条件是否成立成立都要进行变量提升
- 带var 和 function 关键字声明相同的名字,这种也算是重名了(其实是一个fn,只是存储值得类型不一样)
- 关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】
- 暂存死区 typeof a为undefined 可以说明
- 堆内存:存储引用数据类型值(对象:键值对 函数:代码字符串)
- 栈内存:提供JS代码执行的环境和存储基本类型值
- 当前函数执行,形参是私有作用域A,A的上级作用域是谁,和它在哪执行的没有关系,
和它在哪创建有关系,在哪里创建的,它的上级作用域就是谁
。 - 闭包是形成一个不销毁的私有作用域(私有栈内存)
- 闭包有保存和保护作用