本文是lhyt本人原创,希望用通俗易懂的方法来理解一些细节和难点。转载时请注明出处。文章最早出现于本人github
0.前言
主要结合了内存的概念讲了js的一些的很简单、但是又不小心就犯错的地方。
结论:js执行顺序,先定义,后执行,从上到下,就近原则
1.先说类型
在ECMAscript数据类型有基本类型和引用类型,基本类型有Undefined、Null、Boolean、Number、String,引用类型有Object,所有的的值将会是6种的其中之一(数据类型具有动态性,没有定义其他数据类型的必要了)
引用类型的值,也就是对象,一个对象是某个引用类型的一个实例,用new操作符创建也可以用字面量的方式(对象字面量创建var obj ={ })。ECMA里面有很多原生的引用类型,就是查文档的时候看见的那些:Function、Number (是对于原始类型Number的引用类型)、String(是对于原始类型String的引用类型)、Date、Array、Boolean(...)、Math、RegExp等等。
在程序运行的时候,整块内存可以划分为常量池(存放基本类型的值)、栈(存放变量)、很大的堆(存放对象)、运行时环境(函数运行时)
对于如下代码:
var a = 1;
var b = 'hello';
var c = a;
var obj1 = new Object();
obj1.name = 'obj1'
var obj2 = obj1
基本数据类型的值是直接在常量池里面可以拿到,而引用类型是拿到的是对象的引用
c = a,这种基本数据类型的复制,只是重新复制一份独立的副本,在变量的对象上创建一个新的值,再把值复制到新变量分配的位置上,a、c他们自己的操作不会影响到对方。
a++;console.log(a);console.log(c)显然是输出2、1
obj1和obj2,拿到的是新创建的对象的引用(也就是家里的钥匙,每个人带一把),当操作对象的时候,对象发生改变,另一个obj访问的时候,发现对象也会改。就像,家里有一个人回去搞卫生了,另一个回家发现家里很干净了。
console.log(obj2) //'obj1'
函数也是同理
var a = function(){console.log(1)}
var b = a;
a = null;
b();a()
//b输出1,a报错:Uncaught TypeError: a is not a function
把a变成null,只是切断了a和函数之间的引用关系,对b没有影响
2.再说顺序
大家常听说的先定义后执行,其实就是在栈中先开辟一块内存空间,然后在拿到他所对应的值,基本类型去常量池,引用类型去堆拿到他的引用。大家常说的原始类型值在栈,其实就是这种效果。
2.1 为什么引用类型值要放在堆中,而原始类型值要放在栈
栈比堆的运算速度快,Object是一个复杂的结构且可以扩展:数组可扩充,对象可添加属性,都可以增删改查。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。
因此又引出另一个话题,查找值的时候先去栈查找再去堆查找。为什么先去栈查找再去堆查找
简单来说,你宁愿大海捞针呢还是碗里捞针呢?
具体如何,还得问一下语言的底层去了
3.函数
先抛出一个问题
function a(){console.log(2)}; var a = function(){console.log(1)}; a()
覆盖?那么交换的结果又是什么呢?
var a = function(){console.log(1)}; function a(){console.log(2)}; a()
都是1,然后有的人就说了,var优先。好的,那为什么var优先?
先定义后执行,先去栈查找
变量提升,其实也是如此。先定义(开辟一块内存空间,此时值可以说是undefined)后执行(从上到下,该赋值的就赋值,该执行操作的就去操作),就近原则
函数声明和函数表达式,有时候不注意,就不小心出错了
a(); function a(){console.log(666)}//666
另一种情况:
a(); var a = function (){console.log(666)}//a is not a function
虽然第一种方法有变量提升,不会出错,正常来说,还是按顺序写,定义语句放前面。如果想严格要求自己,就手动来个严格模式‘use strict’吧。对于框架的开发,需要严谨遵守规则,所以一般会用严格模式。
4.接着是临时空间
函数执行的时候,会临时开辟一块内存空间,这块内存空间长得和外面这个一样,也有自己的栈堆,当函数运行完就销毁。
4.1 eg1:
var a = 10;
function() {
console.log(a);//undefined
var a = 1;
console.log(a)//1
}
宏观来说,只有2步一和二,当执行第二步,就跳到函数内部执行②-⑧
函数外部的a=10完全就没有关系,这里面造成undefined主要因为变量提升,其实准确的顺序是:
var a
console.log(a);//undefined
a = 1;
console.log(a)//1
为什么不出去找全局的a?
就近原则。为什么就近原则?都确定函数内部有定义了,就不会再去外面白费力气,说到底这个还是得问一下底层怎么实现。类似的一个例子,我们用函数声明定义一个函数f,再用一个变量g拿到这个函数的引用,然后在外面用f是访问不了这个函数的,但是在函数内部是能找到f这个名字的:
var g = function f(){}
f()//报错
我猜想是,这是内部的一种性能优化方法,他不会浪费更多的资源去干一件事,具体是什么原因,为什么就近原则,得问底层原理去了。
4.2 eg2
function f(){
return function f1(){
console.log(1)
}
};
var res = f();
res();
f1()
res(),返回的是里面的函数,如果直接f1()就报错,因为这是window.f1()
函数声明后,可以通过引用名称查找或者内存地址查找
局部作用域用function声明,声明不等于创建,只有调用函数的时候才创建
函数f有内存地址的话,通过栈找f的内存空间,如果找不到栈中f这个变量,就去堆中找
5.IIFE和闭包
5.1 IIFE
立即执行函数,内部就是一个闭包,形成一个沙盒环境,防止变量污染内部,是做各种框架的好方法
先手写一段假的jQuery
(function(root){
var $ = function(){
//代码
}
root.$ = $
})(this)
这样子在内部函数里面写相关的表达式,我们就可以用美元符号使用jQuery(实际上jQuery第一个括号是全局环境判断,真正的函数体放在第二个括号里面,号称世界上最强的选择器sizzle也里面)
5.2闭包
闭包的概念各有各的说法,平时人家问闭包是什么,大概多数人都是说函数中的函数、函数里面能访问到外面的变量。
《权威指南》:函数对象通过作用域链相互关联起来,函数内部变量都可以保持在函数的作用域中,有权访问另一个函数作用域中的变量
《忍者秘籍》:一个函数创建时允许自身访问并操作该自身函数以外的变量所创建的作用域
《你不知道的js》:是基于词法的作用域书写代码时所产生的结果,当函数记住并访问所在的词法作用域,闭包就产生了
其实这是闭包的现象。闭包有现象与产生。闭包的产生,会导致内存泄漏。
js具有垃圾回收机制,如果发现变量被不使用将会被回收,而闭包相互引用,让他不会被回收,一直占据着一块内存,长期持有一块内存的引用,这就是内存泄漏
原文来源于:lhyt的github