堆栈的理解
堆栈的一些概念
- JS执行环境(V8引擎等)
- 执行环境栈(ECStack,execution context stack),浏览器渲染界面时在内存中开辟的一块空间
- 执行上下文,管理不用的区域,每个执行上下文中的代码需要执行时,进栈执行,栈底永远都有一个全局的执行上下文EC(G)
- VO(G),全局变量对象
- 无论是栈内存还是引用类型数据的堆内存,都是属于计算机内存
堆栈对基本数据类型处理
- 基本数据类型是按值进行操作
- 基本数据类型值是存放在栈区的,不会分配16进制的内存地址,也没有开辟内存空间
// 1、创建一个值100,由于100是一个基本数据类型,所以会直接存放在栈区
// 2、声明一个变量x,存放在VO(G)
// 3、建立变量与值之间的联系 x = 100
var x = 100
// 1、声明一个变量y,存放在VO(G)
// 2、建立变量与值之间的联系 y = x = 100
var y = x
// 1、创建一个值200,由于200是一个基本数据类型,所以会直接存放在栈区
// 2、断掉y和100的联系
// 2、建立变量与值之间的联系 y = 200
y = 200
console.log(x)
堆栈对引用类型数据处理
// 1、声明一个变量obj1,存放在VO(G)
// 2、创建一个堆内存heap 0x000,存放对象键值对形式数据,x: 100
// 3、栈区的obj1指向堆内存地址0x000
var obj1 = { x: 100 }
// 栈区的obj2指向obj1同样的堆内存地址0x000
var obj2 = obj1
// 栈区的obj2修改堆内存地址0x000的键x的值为200
obj2['x'] = 200
// 此时0x000地址内的x变成了200,所以obj1打印出来的x为200
console.log(obj1.x)
// 1、声明一个变量obj3,存放在VO(G)
// 2、创建一个堆内存heap 0x000,存放对象键值对形式数据,x: 100
// 3、栈区的obj3指向堆内存地址0x000
var obj3 = { x: 100 }
// 栈区的obj4指向obj3同样的堆内存地址0x000
var obj4 = obj3
// obj4创建并指向一个堆内存heap 0x001,存放对象键值对形式数据,name: tom
obj4 = { name: 'tom' }
// 此时0x000地址内的数据并没有被修改,所以obj3打印出来的x还是100
console.log(obj3.x)
// 1、声明一个变量obj5,存放在VO(G)
// 2、创建一个堆内存heap 0x000,存放对象键值对形式数据,x: 100
// 3、栈区的obj5指向堆内存地址0x000
var obj5 = { x: 100 }
// 栈区的obj6指向obj5同样的堆内存地址0x000
var obj6 = obj5
// 1、obj5.y由于优先运算,在0x000中创建一个y,这个y将来会指向接下来将会创建的内存地址
// 2、obj5 = { x: 200 },此时obj5创建并指向一个堆内存heap 0x001,存放对象键值对形式数据,x: 200
// 3、此时heap 0x000内存内新增了y指向0x001内存内数据{ x: 200 }
obj5.y = obj5 = { x: 200 }
// 此时obj5内只有x,没有y,输出undefined
console.log(obj5.y)
// 此时obj6地址还是0x000,输出{ x: 200 }
console.log(obj6)
堆栈对函数类型处理
函数的创建
- 可以将函数名称看作是变量,存放在VO中,同时它的值就是当前函数对应的内存地址
- 函数本身也是一个对象,创建时会有一个内存地址,空间内存放的就是字符串形式函数体代码
函数执行时会形成一个全新私有上下文,它里面有一个AO(全局执行环境里面叫VO),用于管理这个上下文当中的变量,具体步骤如下
- 确定作用域链<当前执行上下文,上级作用域所在的执行上下文>
- 确定this
- 初始化arguments(对象)
- 形参赋值:它就相当于是变量声明,然后将声明的变量放置于AO
- 变量提升
- 代码执行
// 1、声明一个变量arr,存放在VO(G)
// 2、创建一个堆内存heap 0x001,存放对象键值对形式数据,0: zce,1: tom
// 3、栈区的arr指向堆内存地址0x001
var arr = ['zce', 'tom']
// 1、创建函数和创建变量类型,函数名此时就可以看做是一个变量
// 2、单独开辟一个堆内存用于存放函数体(字符串形式代码),当前内存地址也会有一个16进制数值地址0x000(函数创建优先)
// 3、创建函数的时候,它的作用域[[scope]]就已经确定了(创建函数时所在的执行上下文),此例为EC(G)全局执行上下文
// 4、创建函数之后会将它的内存地址存放在栈区与对应的函数名进行关联
function foo (obj) {
// 函数参数obj指向0x001的地址,修改0x001地址上0: zoe
obj[0] = 'zoe'
// 重新创建一个堆地址0x002,0: javascript,obj指向0x002
obj = ['javascript']
// 0x002地址下新增1: typescript
obj[1] = 'typescript'
// 打印0x002地址数据 ['javascript', 'typescript']
console.log(obj)
}
// 函数执行,目的就是为了将函数对应的堆内存里的字符串形式代码进行执行。代码在执行的时候肯定需要一个环境,此时就意味着函数在执行的时候会生成一个新的执行上下文EC(foo)来管理函数体当中的代码
// 执行函数相当于找到foo的内存地址0x000,然后吧参数的内存地址0x001传入,执行0x000(0x001)
foo(arr) // 执行完成后,没有其他引用关系,出栈
// 打印0x001地址数据 [‘zoe’, 'tom']
console.log(arr)
堆栈对闭包类型处理
闭包类型堆栈处理
- 闭包是一种机制,通过私有上下文来保护当中变量的机制
- 我们也可以认为当我们创建的某一个执行上下文不被释放的时候就形成来闭包
- 保护:使当前上下文中的数据和其他上下文中的数据互不干扰,保存数据:当前上下文的数据(堆内存)被当前上下文以外的上下文中的变量所引用,这个数据就保存下来
- 函数调用形成来一个全新的私有上下文,在函数调用之后当前上下文不被释放或临时不被释放就是闭包
// 全局执行上下文中的VO中声明a = 1
var a = 1
// 为函数foo创立一个堆地址0x000,全局执行上下文中的VO中声明foo指向0x000,确定作用域为EC(G)
// foo函数执行时,会创立一个EC(FOO)的执行上下文,并在其中的AO中声明变量b=2
function foo () {
var b = 2
// 函数执行完时返回了一个函数。此时创建一个新的存放此匿名函数的堆地址0x001
return function (c) {
console.log(c + b++)
}
}
// 当前ec(foo)执行上下文当中引用的一个堆地址0x001被ec(g)中的变量f引用,因此foo()调用时所创建的执行上下文不鞥被释放,此时就形成了闭包
// foo函数执行完毕返回了0x001地址的函数,所以f指向0x001
var f = foo()
// f函数执行时创建新的执行上下文ec(f),并在其AO中声明参数c=5,在上级作用域中找到b,打印5+2=7,b自增为3
f(5)
// 函数每调用一次会产生一个全新的上下文,此时函数执行产生新的执行上下文ec(f),并在其AO中声明参数c=10,在上级作用域中找到b,打印10+3=13,b自增为4
f(10)
相关优化的一些小技巧
对for循环的一些优化
// 查找到数组形式的列表,存入堆地址0x000,然后为数组中每一个btn开辟堆内存0x001-0x002-0x003,0x000中的数据指向这三个地址
var btns = document.querySelectorAll('button')
// 基础版本,全部都输出3
for (var i = 0; i < btns.length; i++) {
// 依次循环时创建匿名函数地址0x101-0x102-0x103,让每一个btn的onclick指向对应的地址,然后i值每次+1,最后i=3,所以每个按钮点击时都会找到它们上级的i,输出3
btns[i].onclick = function () {
console.log(i)
}
}
// 使用闭包改造
for (var i = 0; i < btns.length; i++) {
// 自执行函数会创立一个新的堆空间0x201,以及三个执行上下文EC(AN1)-EC(AN2)-EC(AN3),并把每次循环的i值声明在对应的执行上下文AO里
// 闭包的方式只有进栈,没有出栈,会消耗很多内存空间,此时要在循环结束后加上btns=null来释放内存
(function (i) {
btns[i].onclick = function () {
console.log(i)
}
})(i)
}
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function (i) {
return function () {
console.log(i)
}
})(i)
}
// 使用let,let原理和闭包类型,产生独立作用域
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log(i)
}
}
// 使用自定义属性
for (var i = 0; i < btns.length; i++) {
// 往已经存在的内存地址0x00i上面添加一个属性值btnIndex存放i值,不会额外开辟内存空间
btns[i].btnIndex = i
btns[i].onclick = function () {
console.log(this.btnIndex)
}
}
// 使用事件委托,最省内存
document.body.onclick = function (ev) {
var target = ev.target,
targetDom = target.tagName
if (targetDom === 'BUTTON') {
var index = target.getAttribute('index')
console.log(index)
}
}
变量局部化优化
可以提高代码的执行效率(减少了数据访问时需要照的路径)
var i, str = ''
// 创建foo1的堆地址0x001
function foo1() {
for (i = 0; i < 1000; i++) {
str += i
}
}
// 创建foo2的堆地址0x002
function foo2() {
let str = ''
for (let i = 0; i < 1000; i++) {
str += i
}
}
// 执行时,foo1的执行上下文EC(foo1)中并没有变量str和i,需要到全局执行上下文EC(G)中寻找,会消耗很多时间
foo1()
// 执行时,foo1的执行上下文EC(foo2)中有变量str和i,会节省很多时间
foo2()
缓存数据与循环体优化
- 减少声明和语句数(词法 语法)
- 缓存数据(作用域链查找更快)
const arr = Array(1000).fill(3)
// 循环体优化,将循环体内多次用到的length属性保存
// for (let i = 0; i < arr.length; i++) {
// console.log(i)
// console.log(arr.length)
// }
const len = arr.length;
for (let i = 0; i < len; i++) {
console.log(i);
console.log(len)
}
// 循环体终极优化,while从后往前遍历
while (len--) {
console.log(i);
console.log(len)
}
减少访问层级
function Person() {
this.name = 'tom'
this.age = 26
this.getAge = function () {
return this.age
}
}
let p1 = new Person()
console.log(p1.age)
console.log(p1.getAge())
字面量与构造式
字面量比构造式的速度更快,尤其是基本数据类型声明时
var test1 = () => {
let obj = new Object()
obj.name = 'tom'
obj.age = 32
obj.slogan = 'haha'
}
var test2 = () => {
let obj = {
name: 'tom',
age: 32,
slogan: 'haha',
}
}
var str1 = 'hahah'
var str2 = new String('hahah')
减少判断层级
优化前
function doSomething (part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (part) {
if (parts.includes(part)) {
console.log('包含')
if (chapter > 5) {
console.log('需开通VIP')
}
}
} else {
console.log('请确认信息')
}
}
doSomething('ES2016', 6)
优化后
function doSomething (part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (!part) {
console.log('请确认信息')
return
}
if (!parts.includes(part)) return
console.log('包含')
if (chapter > 5) {
console.log('需开通VIP')
}
}
doSomething('ES2016', 6)