通过自己的总结和学习,记录的一点笔记
声明变量
- 先声明一个变量
a
,没有赋值 - 在当前作用域开辟一个位置存储
12
- 让变量
a
和12
关联在一起(定义:赋值) - 基础值类型就是直接操作值,和变量没有关系
- 引用类型是按照引用的空间地址进行操作,地址都是一个。
- 函数也是引用类型
- 栈内存:作用域(代码都是在栈中执行的) 1. 提供一个提供 js 代码自上而下执行的环境 2. 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的 3. 栈内存被销毁,存储的基本值也销毁
- 堆内存:引用值对应的空间 1. 存储引用类型值的(对象:键值对 函数:代码字符串) 2. 当前堆内存销毁,那么引用消失 3. 堆内存没有被任何变量或者其他东西占用,就会被浏览器在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁 4. 销毁堆:XX=null
变量提升
当栈内存(作用域)形成,js 代码自上而下执行之前,浏览器首先会把所有带
var
和function
关键词的进行提前的“声明”或者“定义”,这种预先处理机制称之为“变量提升”- 声明(declare):
var a/function sum
(默认值 undefined) - 定义(defined):
a=12
(定义其实就是赋值) - 变量提升阶段:带
var
的只声明未定义,带funtion
的声明和赋值都完成了
- 声明(declare):
变量提升只发生在当前作用域(例如:开始加载页面的时候只对全局作用域下的进行提升,因为 此时函数中存储的都是字符串而已)
- 在全局作用域下声明的函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变 量”(带
var/function
的才是声明) - 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
- 私有作用域形成后,也不是立即执行代码,而是先进行变量提升(变量提升钱,先形参赋值)
- 在
es3/es5
语法规范中,只有全局作用域和函数执行的私有作用域(栈内存),其他大括号不会形成栈内存
带 var 和不带 var 的区别
- 在全局作用域下声明一个变量,也相当于给
window
全局对象设置了一个属性,变量的值就是属性值(私用作用域中声明的私有变量和window
没啥关系) - 在变量提升阶段,在全局作用域中声明了一个变量
a
,此时就已经把a
当做属性赋值给window
了,只不过此时还没有给a
赋值,默认undefined
in
操作符可以检测某个属性是否属于这个对象- 全局变量和
window
中的属性存在“映射机制” a=12=>window.a=12
不加var
的本质是window
的属性。var a=b=12
这样写b
是不带var
的- 私有作用域中带 var 和不带也有区别
- 带
var
的在私有作用域变量提升阶段,都声明为私有变量,和外界没有任何的关系 - 不带
var
不是私有变量,会向它的上级作用域查找,一直找到window
为止(我们把这种查找机制叫做:‘作用域链’),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的。
- 带
### 作用域链的扩展
- 在作用域链查找的过程中,如果找到
window
也没有这个变量,相当于给window
设置一个属性
变量提升的一些细节问题(关于条件判断下的处理)
- 匿名函数之函数表达式
var fn = function () {}
var fn
只对左边进行变量提升- (条件判断下)在当前作用域下,不管条件是否成立都要进行变量提升,带
var
的还是只声明,带function
的在老版本浏览器渲染机制之下,声明加定义都处理了,但是为了迎合es6
中的块级作用域, 新版浏览器对于函数(在条件判断中的函数),不管条件是否成立,都只是先声明,没有定义,类似var
4.
1 | // 没有变量提升 |
条件判断下的变量提升到底有多坑
1 | // 变量提升:function fn; |
变量提升机制下重名的处理
- 带
var
和function
关键字声明相同的名字,这种也算是重名了(其实是一个变量,只是存储值的类型不一样) - 关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)<不管是变量的提升还是代码执行阶段皆是如此>
- 代码有报错,下面的代码不在执行
ES6 中的 let 不存在变量提升
- 在
es6
中基于let/const
等方式创建变量或者函数,不存在变量提升机制,切断了全局变量和window
属性的映射机制 - 在相同的作用域中,基于
let
不能声明相同名字的变量(不管用什么方式在当前作用域下声明了变量,再次使用 let 创建都会报错) - 虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测(语法检测):自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会再执行了(虽然没有把变量提前升声明定义,但是浏览器已经记住了,当前作用域下有哪些变量)
暂时性死区
- 基于
let
创建变量,会把大部分{}当做一个私有的块级作用域(类似于函数的私有作用域),在这里也是重新检测语法规范,看一下是否基于新语法规范创建的变量,如果是则按照新语法规范来解析 - 在原有浏览器渲染机制下,基于 typeof 等逻辑运算符检测一个未被声明过的变量,不会报错,返回
undefined
- 如果当前变量是基于
es6
语法处理,在没有声明这个变量的时候,使用typeof
检测会直接报错,不会undefined
解决了原有的js
的暂时性死区
区分私有变量和全局变量
- 在私有作用域中,只有以下两种情况是私有变量:A.声明过的变量(带 var/function)B.形参也是私有变量; 剩下的都不是自己私有的变量,都需要基于作用域链的机制向上查找
上级作用域的查找
- 当前函数执行,形成一个私有作用域 A,A 的上级作用域是谁,和他在哪执行的没有关系,和他在哪创建(定义)的有关系,在哪创建的,他的上级作用域就是谁
arguments
是函数实参集合,arguments.callee
是函数本身,arguments.callee.caller
是当前函数在哪执行的,caller
就是谁(记录的是他执行的宿主环境),在全局下执行caller
的结果是null
闭包及堆栈内存释放
js
中的内存分为堆内存和栈内存
堆内存:存储引用数据类型值(对象:键值对 函数:代码字符串)
栈内存:提供js
代码执行的环境和存储基本类型值堆内存释放:让所有引用堆内存空间地址的变量赋值为
null
即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放掉)
栈内存释放: 一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也会释放掉),但是也有特殊不销毁的情况:- 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面找不到原有的内容了)
- 全局栈内只有在页面关闭的时候才会被释放掉,如果当时栈内存没有被释放,那么之前在栈内存中存储的基本值也不会被释放,能够一直保存下来。
var f = fn(2)
=>先把fn
执行(传递实参 2),把fn
执行的返回结果(return 后面的值)赋值给f
,f()
=>把返回的结果执行。fn(2)()
=>和上面两步骤类似,都是先把fn
执行,在fn
执行的返回结果再次执行i++
自身累加1
: 先运算在累加++i
自身累加1
: 先累加在运算
闭包的应用
- 闭包:
- 函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为闭包
- 市面上的开发者认为的闭包:形成一个不销毁的私有作用域(私有栈内存)才是闭包
- 闭包:柯理化函数
1 | function fn() { |
- 闭包项目实战应用
真实项目中为了保证 js 的性能(堆栈内存的性能优化),应该尽可能的减少闭包的使用(不销毁的堆栈内存是耗性能的)
闭包具有保护作用:保护私有变量不受外界的干扰 jq
在真实项目中,尤其是团队协作开发的时候,应当尽可能的减少全局变量的使用,以防止相互之间的冲突(‘全局变量污染’),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量(function(){
var n =12
function fn(){}
})()不仅如此,我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用,这样我们如何出理?
- jq 这种方式:把需要暴露的方法抛到全局
1
2
3
4
5
6
7
8
9
10(function(){
function jQuery() {
...
}
...
把需要提供外面使用的方法,通过给win设置属性的方式暴露出去
window.jQuery = window.$ = jQuery;
})()
jQuery();
$()- Zepto 这种方式:基于 return 把需要供外面使用的方法暴露出去
1
2
3
4
5
6
7
8
9var Zepto = (function(){
...
return {
xxx: function(){
}
}
})();
Zepto.xxx()闭包具有保存作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用
- 闭包作用之保存
- 循环和判断都不会形成私有作用域(栈内存),
for
循环执行,形成一个私有的栈内存,遇到变量i
,i
不是私有变量,向上一级作用域查找(上级作用域是 win)
- 所有的事件绑定都是异步编程(同步编程:一件事一件事的做,当前这件事没有完成,下一个任务不能处理/异步编程:当前这件事件没有彻底完成,不再等待,继续执行下面的任务),绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束(让全局的等于循环最后的结果)
解决方案:
- 自定义属性
1
2
3
4
5
6
7for (var i = 0,i<tabList.length;i++){
tabList[i].myIndex = i;
tabList[i].onclick = function(){
changeTab(this.myIndex)
}
// this:给当前元素的某个时间绑定方法,当事件触发,方法执行的时候,方法中的this是当前操作的元素对象
}- 闭包解决
1
2
3
4
5
6
7
8
9
10
11for (var i = 0,i<tabList.length;i++){
tabList[i].onclick = (function(n){
// 让自执行函数执行,把执行的返回值(return)赋值给此处on-click(on-click绑定的是返回的小函数,点击的时候执行的是小函数),自执行函数在给事件赋值的时候就已经执行了
var i=n;
return function () {
changeTab(i); // 上级作用域:自执行函数形成的作用域
}
})(i)
}
// 总结:循环几次,形成几个不销毁的私有作用域(自执行函数执行),而每一个不销毁的栈内存中都存储了一个私有变量,而这个值分别是每一次执行传递进来的全局i的值,当点击的时候,执行返回的小函数,遇到变量i,向它自己的上级作用域查找,找到i的值- 基于 ES6 解决
1
2
3
4
5
6
7
8
9for (let i = 0,i<tabList.length;i++){
// 循环体也是块级作用域,初始值设置的变量是当前本次块级作用域中的变量(形成了i个块级作用域,每个块级作用域中都有一个私有变量i,变量值就是每一次循环的i)
tabList[i].onclick = function(){
changeTab(i)
}
}
// 基于es6中的let来创建变量,是存在块级作用域的(类似于私有作用域)
// 作用域(栈内存)1. 全局作用域 2.私有作用域 3.块级作用域(一般用大括号包起来的都是块级作用域,前提是es6语法规范)
// 对象的{}不是块级作用域