本文是lhyt本人原創(chuàng),希望用通俗易懂的方法來理解一些細節(jié)和難點。轉(zhuǎn)載時請注明出處。文章最早出現(xiàn)于本人github
0.前言
主要結(jié)合了內(nèi)存的概念講了js的一些的很簡單、但是又不小心就犯錯的地方。
結(jié)論:js執(zhí)行順序,先定義,后執(zhí)行,從上到下,就近原則
1.先說類型
在ECMAscript數(shù)據(jù)類型有基本類型和引用類型,基本類型有Undefined、Null、Boolean、Number、String,引用類型有Object,所有的的值將會是6種的其中之一(數(shù)據(jù)類型具有動態(tài)性,沒有定義其他數(shù)據(jù)類型的必要了)
引用類型的值,也就是對象,一個對象是某個引用類型的一個實例,用new操作符創(chuàng)建也可以用字面量的方式(對象字面量創(chuàng)建var obj ={ })。ECMA里面有很多原生的引用類型,就是查文檔的時候看見的那些:Function、Number (是對于原始類型Number的引用類型)、String(是對于原始類型String的引用類型)、Date、Array、Boolean(...)、Math、RegExp等等。
在程序運行的時候,整塊內(nèi)存可以劃分為常量池(存放基本類型的值)、棧(存放變量)、很大的堆(存放對象)、運行時環(huán)境(函數(shù)運行時)

對于如下代碼:
var a = 1;
var b = 'hello';
var c = a;
var obj1 = new Object();
obj1.name = 'obj1'
var obj2 = obj1
基本數(shù)據(jù)類型的值是直接在常量池里面可以拿到,而引用類型是拿到的是對象的引用
c = a,這種基本數(shù)據(jù)類型的復制,只是重新復制一份獨立的副本,在變量的對象上創(chuàng)建一個新的值,再把值復制到新變量分配的位置上,a、c他們自己的操作不會影響到對方。
a++;console.log(a);console.log(c)顯然是輸出2、1
obj1和obj2,拿到的是新創(chuàng)建的對象的引用(也就是家里的鑰匙,每個人帶一把),當操作對象的時候,對象發(fā)生改變,另一個obj訪問的時候,發(fā)現(xiàn)對象也會改。就像,家里有一個人回去搞衛(wèi)生了,另一個回家發(fā)現(xiàn)家里很干凈了。
console.log(obj2) //'obj1'

函數(shù)也是同理
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和函數(shù)之間的引用關(guān)系,對b沒有影響
2.再說順序
大家常聽說的先定義后執(zhí)行,其實就是在棧中先開辟一塊內(nèi)存空間,然后在拿到他所對應的值,基本類型去常量池,引用類型去堆拿到他的引用。大家常說的原始類型值在棧,其實就是這種效果。

2.1 為什么引用類型值要放在堆中,而原始類型值要放在棧
棧比堆的運算速度快,Object是一個復雜的結(jié)構(gòu)且可以擴展:數(shù)組可擴充,對象可添加屬性,都可以增刪改查。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查找到堆中的實際對象再進行操作。
因此又引出另一個話題,查找值的時候先去棧查找再去堆查找。為什么先去棧查找再去堆查找
簡單來說,你寧愿大海撈針呢還是碗里撈針呢?
具體如何,還得問一下語言的底層去了
3.函數(shù)
先拋出一個問題
function a(){console.log(2)}; var a = function(){console.log(1)}; a()
覆蓋?那么交換的結(jié)果又是什么呢?
var a = function(){console.log(1)}; function a(){console.log(2)}; a()
都是1,然后有的人就說了,var優(yōu)先。好的,那為什么var優(yōu)先?

先定義后執(zhí)行,先去棧查找
變量提升,其實也是如此。先定義(開辟一塊內(nèi)存空間,此時值可以說是undefined)后執(zhí)行(從上到下,該賦值的就賦值,該執(zhí)行操作的就去操作),就近原則
函數(shù)聲明和函數(shù)表達式,有時候不注意,就不小心出錯了
a(); function a(){console.log(666)}//666
另一種情況:
a(); var a = function (){console.log(666)}//a is not a function
雖然第一種方法有變量提升,不會出錯,正常來說,還是按順序?qū)?,定義語句放前面。如果想嚴格要求自己,就手動來個嚴格模式‘use strict’吧。對于框架的開發(fā),需要嚴謹遵守規(guī)則,所以一般會用嚴格模式。
4.接著是臨時空間
函數(shù)執(zhí)行的時候,會臨時開辟一塊內(nèi)存空間,這塊內(nèi)存空間長得和外面這個一樣,也有自己的棧堆,當函數(shù)運行完就銷毀。
4.1 eg1:
var a = 10;
function() {
console.log(a);//undefined
var a = 1;
console.log(a)//1
}
宏觀來說,只有2步一和二,當執(zhí)行第二步,就跳到函數(shù)內(nèi)部執(zhí)行②-⑧

函數(shù)外部的a=10完全就沒有關(guān)系,這里面造成undefined主要因為變量提升,其實準確的順序是:
var a
console.log(a);//undefined
a = 1;
console.log(a)//1
為什么不出去找全局的a?
就近原則。為什么就近原則?都確定函數(shù)內(nèi)部有定義了,就不會再去外面白費力氣,說到底這個還是得問一下底層怎么實現(xiàn)。類似的一個例子,我們用函數(shù)聲明定義一個函數(shù)f,再用一個變量g拿到這個函數(shù)的引用,然后在外面用f是訪問不了這個函數(shù)的,但是在函數(shù)內(nèi)部是能找到f這個名字的:
var g = function f(){}
f()//報錯
我猜想是,這是內(nèi)部的一種性能優(yōu)化方法,他不會浪費更多的資源去干一件事,具體是什么原因,為什么就近原則,得問底層原理去了。
4.2 eg2
function f(){
return function f1(){
console.log(1)
}
};
var res = f();
res();
f1()
res(),返回的是里面的函數(shù),如果直接f1()就報錯,因為這是window.f1()

函數(shù)聲明后,可以通過引用名稱查找或者內(nèi)存地址查找
局部作用域用function聲明,聲明不等于創(chuàng)建,只有調(diào)用函數(shù)的時候才創(chuàng)建
函數(shù)f有內(nèi)存地址的話,通過棧找f的內(nèi)存空間,如果找不到棧中f這個變量,就去堆中找
5.IIFE和閉包
5.1 IIFE
立即執(zhí)行函數(shù),內(nèi)部就是一個閉包,形成一個沙盒環(huán)境,防止變量污染內(nèi)部,是做各種框架的好方法
先手寫一段假的jQuery
(function(root){
var $ = function(){
//代碼
}
root.$ = $
})(this)
這樣子在內(nèi)部函數(shù)里面寫相關(guān)的表達式,我們就可以用美元符號使用jQuery(實際上jQuery第一個括號是全局環(huán)境判斷,真正的函數(shù)體放在第二個括號里面,號稱世界上最強的選擇器sizzle也里面)

5.2閉包
閉包的概念各有各的說法,平時人家問閉包是什么,大概多數(shù)人都是說函數(shù)中的函數(shù)、函數(shù)里面能訪問到外面的變量。
《權(quán)威指南》:函數(shù)對象通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)內(nèi)部變量都可以保持在函數(shù)的作用域中,有權(quán)訪問另一個函數(shù)作用域中的變量
《忍者秘籍》:一個函數(shù)創(chuàng)建時允許自身訪問并操作該自身函數(shù)以外的變量所創(chuàng)建的作用域
《你不知道的js》:是基于詞法的作用域書寫代碼時所產(chǎn)生的結(jié)果,當函數(shù)記住并訪問所在的詞法作用域,閉包就產(chǎn)生了
其實這是閉包的現(xiàn)象。閉包有現(xiàn)象與產(chǎn)生。閉包的產(chǎn)生,會導致內(nèi)存泄漏。
js具有垃圾回收機制,如果發(fā)現(xiàn)變量被不使用將會被回收,而閉包相互引用,讓他不會被回收,一直占據(jù)著一塊內(nèi)存,長期持有一塊內(nèi)存的引用,這就是內(nèi)存泄漏
原文來源于:lhyt的github