夯实基础,彻底掌握js的核心技术(三):堆栈内存及闭包详解
数据渲染机制及堆栈内存
1. 数据值操作机制
/*
* 1. 先声明一个变量a,没有赋值(默认值谁undefined)
* 2. 在当前作用域中开辟一个位置存储12这个值
* 3. 让变量a和12关联在一起(定义:赋值)
*/
var a = 12
var b = a;
b = 13;
console.log(a);
var ary1 = [12, 23];
var ary2 = ary1;
ary2.push(100);
console.log(ary1)
function sum() {
var total = null;
for(var i = 0; i< arguments.length; i++;) {
var item = arguments[i];
item = parseFloat(item);
!isNaN(item) ? total += item : null;
}
return total;
}
console.log(sum(12, 23, '34', 'AA'))
解析如下图:<br />
image.png <br />栈内存:作用域- 提供一个供js代码自上而下执行的环境(代码都是在栈中执行)
- 由于基本数据类型值比较简单,它们都是直接在栈内存中开辟一个位置,把值直接存储进去的
- 当栈内存被销毁,存储的那些基本值也都跟着被销毁了
堆内存:引用值对应的空间
- 存储引用类型值的(对象:键值堆 函数:代码字符串)
- 当前堆内存释放销毁,那么这个引用值彻底没了
- 堆内存的释放: 当堆内存没有被任何的变量或者其它东西所占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁掉(谷歌浏览器)(xxx= null,通过空对象指针null可以让原始变量(或者其它东西)谁都不指向,那么原有被占用的堆内存就没有被东西占用了,浏览器会销毁它)
2. 变量提升机制
- 什么事变量提升
变量提升: 当栈内存(作用域)形成,js代码自上而下执行之前,浏览器首先会把所有带va r/function关键词进行提前“声明”或者“定义”,这种预先处理机制称为“变量提升”<br />声明:(declare):var a/ function sum<br />定义:(defined)a= 12 (定义其实就是赋值操作)<br />在变量提升阶段:<br />1. 带“var”的只声明未定义 <br />2. 带“function”的声明和赋值都完成了
- 变量提升只发生在当前作用域
- 在全局作用域下声明函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变量”(带var/function的才是声明)
- 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
var a = 12
var b = a;
b = 13;
console.log(a);
var ary1 = [12, 23];
var ary2 = ary1;
ary2.push(100);
console.log(ary1)
function sum() {
var total = null;
for(var i = 0; i< arguments.length; i++;) {
var item = arguments[i];
item = parseFloat(item);
!isNaN(item) ? total += item : null;
}
return total;
}
console.log(sum(12, 23, '34', 'AA'))
变量提升机制解析:<br />
image.png
- 带var和不带的区别
在全局作用域下声明一个变量,也相当于给window全局设置了一个属性,变量的值就是属性值(私有作用域中的声明的私有变量和window没啥关系)
console.log(a) // undefined
console.log(window.a) // undefined
console.log('a' in window) // true 在全局作用域中声明了一个变量a,此时就已经把a当成属性赋值给window了,只不过此时还没有给a
//赋值,默认值undifined in:检测某个属性是否隶属于这个对象
var a = 12; // 全局变量值修改,window的属性值也跟着修改
console.log(window.a) // window的一个属性名a 12
a = 13
console.log(window.a) // 13
window.a = 14;
console.log(a) // 14
以上例子说明:全局变量和window中的属性存在“映射机制”<br />**<br />不加var的本质是window的属性<br />如下面例子:
console.log(a) // Uncaught ReferenceError: a is not defined
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 a = 12,
b = 13; // 这样写b是带var的
var a = b = 12; // 这样写b是不带var的
私有作用域中带var和不带var也有区别:
- 带var的在私有作用域变量提升阶段,都声明为私有变量,和外界么有任何关系
- 不带var的不是私有变量,会向它的上级作用域查找,看是否为上级变量,不是,继续向上查找,一直查找到window为止(我们把这种查找机制叫做:“作用域”),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的
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
解析如下图:<br />
image.png <br />作用域链的扩展:function fn() {
b= 13;
console.log('b' in window) // true,在作用链查找的过程中,如果找到window也没有这个变量,相当于给
// 给window设置了一个属性b(window.b = 13)
console.log(b)
}
console.log(b)
- 等号左边变量提升
/*
* 变量提升:
* var fn; 只对等号左边进行变量提升
**/
sum();
fn(); // Uncaught TypeError: fn is not a function
// 匿名函数之函数表达式
var fn = function () {
console.log(1)
}
// 普通函数
function sum() {
console.log(2)
}
- 条件判断下的变量提升
在当前作用域下,不管条件是否成立都要进行变量提升
- 带var的还只是声明
- 带function的在老版本浏览器渲染机制下,声明+定义,但是为了迎合ES6中的块级作用域,新浏览器对于函数(在条件判断中的函数)是否成立,都只是先声明,没有定义,类似var
- 在全局作用域下声明的全局变量也相当于给window设置了一个属性
/**
* 在当前作用域下,不管条件是否成立都要进行变量提升
* 带var的还只是声明
* 带function的在老版本浏览器渲染机制下,声明+定义,但是为了迎合ES6中的块级作用域,新浏览器对于函数(在条件判断中的函数)
是否成立,都只是先声明,没有定义,类似var
*/
console.log(a) // undefined
if(1 === 2) {
var a = 12
}
console.log(a) //undefined
//在全局作用域下声明的全局变量也相当于给window设置了一个属性
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() && ([] == ![])) { // Uncaught TypeError: g is not a function [] == ![] => 0 == 0
// 把全局中的f进行修改
f= function () {
return false
};
function g() {
return true;
}
}
}()
console.log(f());
console.log(g());
/*
* 全局下变量提升
* function fn
*/
console.log(fn) // undefined
if(1 === 1) {
console.log(fn)
function fn() {
console.log('ok')
}
}
console.log(fn) //函数本身
- 重名问题的处理
- 带var 和function 关键字声明相同的名字,这种也算事重名了(其实是一个,只是存储的值类型不一样)
- 关于重名处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)【不管是变量提升还是代码执行阶段都是如此】
fn(); // 4
function fn() {
console.log(1);
}
fn(); // 4
function fn() {
console.log(2);
}
fn(); // 4
var fn = 100 // 带var的在提升阶段只把声明处理了,赋值操作没处理,所在在代码执行的时候需要完成赋值100
fn(); // Uncaught TypeError: fn is not a function
function fn() {
console.log(3);
}
fn();
function fn() {
console.log(4);
}
fn();
3. ES6中let不存在变量提升
- 不允许重复定义
- 不存在变量提升
- 在ES6中基于let/const等方式创建变量或者函数,不存在变量提升机制
- 切断了全局变量和window属性的映射机制
- 在相同的作用域中,基于let不能声明相同的名字的变量(不管用什么方式在当前作用域下声明了变量,再次使用let创建都会报错)
- 虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测,自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会再执行了(虽然没有把变量提升声明定义,但是浏览器已经记住了,当前作用下有哪些变量)
console.log(a) //Uncaught ReferenceError: a is not defined
let a = 12;
console.log(window.a) // undefined
console.log(a) // 12
//-------------
let a = 10,
b = 10
let fn =function () {
// console.log(a, b) //Uncaught ReferenceError: a is not defined
let a = b = 20
console.log(a, b) // 20, 20
}
fn();
console.log(a, b) // 10, 20
//-------------
let a = 12
console.log(a)
let a = 13 // Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a)
- 暂时性死区
- 基于let创建变量,会把大部分{}当作一个私有块级作用域(类似函数的私有作用域),在这里也是重新检测语法规范,看一下是否基于新语法创建的变量,如果是按照新语法规范来解析
- es6解决了浏览器的暂时性死区问题
var a = 12;
if(true) {
console.log(a)
let a = 13
}
//--------
console.log(typeof a) // "undefined" 在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明的
// 变量不会报错,返回undefined
//--------
console.log(typeof a) // Uncaught ReferenceError: a is not defined
let a //如果当前变量是基于es6语法处理,在没有声明这个变量的时候,使用typeof检测会直接报错,不会是undefined,解决了原有的js死区问题
闭包作用域(scope)
1. 区分私有变量和全局变量
在私有作用域中,只有以下两种情况是私有变量:
- 声明过的变量(带var/function)
- 行参也是私有变量
剩下的都不是自己私有变量,都需要基于作用域链的机制向上查找
var a = 12,
b = 13,
c = 14;
function fn(a) {
/*
* 行参赋值 a = 12
* 变量提升: var b
* 在私有作用域中,只有以下两种情况是私有变量
* 1.声明过的变量(带var/function)
* 2.行参也是私有变量
* 剩下的都不是自己私有变量,都需要基于作用域链的机制向上查找
**/
console.log(a, b, c) // 12, undefined, 14
var b = c = a = 20; // var b = 20; c=20; 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) {
console.log(ary) //[12, 23]
ary[0] = 100; //[100, 23]
ary = [100] // [100]
ary[0] = 0 // [0]
console.log(ary) //[0]
}
fn(ary)
console.log(ary) //[100, 23]
解析如下图:<br />
image.png
2. 查找上级作用域
- 当前函数执行,形成一个私有作用域A,A的上级作用域是谁,和它在哪执行的没有关系,和它在哪创建的有关系,在哪创建(定义)的,它的上级作用域就是谁
var a = 12
function fn() {
// arguments:实参集合,arguments.callee:函数本身fn
console.log(a)
}
function sum() {
var a = 120
fn(); // 12
}
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
image.png
<br />
image.png
<br />
image.png
3. 闭包及堆栈内存释放
js中的内存分为堆内存和栈内存<br />堆内存:存储引用数据类型值(对象:键值对 函数: 代码字符串)<br />栈内存:提供js代码执行的环境和存储基本类型数据<br />【堆内存释放】<br />让所引用的堆内存空间地址的变量赋值为null即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放)<br />【栈内存释放】<br />一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:
- 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放,外面找不到原有的内容了)。
- 全局栈内存只有在页面关闭的时候才会被释放掉
如果当前栈内存没有释放,那么之前在栈内存中存储的基本值也不会释放,能够一直保存下来
var i = 1;
function fn(i) {
return function (n) {
console.log(n+ (++i));
}
}
var f = fn(2); //先把fn执行(传递实参2),把fn执行的返回结果(return后面的值)赋值股f
f(3) // 把返回的结果执行 =》 6
fn(5)(6) //12 和上面两个步骤类似,都是把fn执行,都是先把fn执行,把fn执行的返回的结果再执行
fn(7)(8) // 16
f(4) //8
//---------
// 在和其它值进行运算的时候一些区别
// i++ ;自身累加1(先拿原有值进行运算,运算结束后,本身累加)
// i++; 自身累加1 (先自身累加1,拿累加后的结果进行运算)
var k = 1;
console.log(5+(k++), k) // 6, 2
console.log(5+ (++k), k) //7, 2
//--------
解析如下图:<br />
image.png
<br />
image.png
4. 闭包之保护机制
闭包:<br />函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为“闭包”。<br />市面上的开发者认为的闭包是:形成一个不销毁的私有作用域(私有栈内存)才是闭包。<br />
// 闭包:柯理化函数
function fn() {
return function(){
}}
var f = fn()
//闭包:惰性函数
var utils = (function(){
return {
}
})()
真实项目中为保证js的性能(堆栈内存的性能优化),应该可能减少闭包的使用(不销毁的堆栈内存是耗性能的)<br />闭包保护功能:
- 闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目只能够,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。
- jQuery方式:把需要暴露的方法抛到全局
- zepto这种方式:基于return把需要供外面使用的方法暴露出去
- 闭包具有“保护”作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用