學習js也有一段時間了,可是往往提到執行上下文、做用域、閉包、變量提高、this等關鍵詞時心中老是有一個模糊的概念,好像知道又好像不知道,所以我想和你們系統的討論這幾個概念。但願可以幫到和我同樣還爲這幾個熟悉而陌生的詞感到苦惱的同窗!javascript
這裏我不打打算一開始就討論上面那些概念,每種語言都有內建的數據類型,不一樣的創建方式也意味着不同的使用方式。而是從js的數據類型開始一步一步分析,則可讓你摸清楚上面幾個概念的前因後果。前端
若是你細心的留意過js語言的一門細節,就會發現js是一門弱類型的動態語言。vue
在js代碼會在if判斷語句中自動將表達式計算成布爾類型的值,同時在js中聲明的變量的定義無需肯定它是字符串、數字或者布爾等其餘類型,這意味着你能夠在一個變量中保存不一樣類型的數據。值得一提的是這種動態語言的類型再帶來極大便利性的同時也會帶來一些使人困擾的問題,在vue這門優秀的框架中使用了Flow對js作了靜態類型語言檢查。java
上面咱們知道了js是一門弱類型的動態語言,那麼咱們接下來看看js中的數據類型python
在js中數據類型分爲基本類型和引用類型:面試
(1)基本類型有:瀏覽器
(2)js的引用類型是從object的子類型,有以下幾種:緩存
js中對不一樣類型的數據的操做不是相同的,要想理解其中的差別,先得搞清楚js中存儲模型。(從極客上拿的圖)markdown
在js執行的過程當中,主要有三種類型內存空間,分別是:代碼空間、棧空間、堆空間閉包
基本類型的數據類型都存儲在棧空間,引用類型的值保存在堆中的。
// 定義四個變量
var num1 = 5
var num2 = num1;
var obj1 = {
name: '小豬皮皮呆'
}
var obj2 = obj1
// 修改num1和obj1
num1 = 4
obj1.name = '小豬'
// 輸出四個變量
console.log(num1) // 4
console.log(num2) // 5
console.log(obj1.name) // 小豬
console.log(obj2.name) // 小豬
複製代碼
上面代碼num1和num2的輸出咱們可以很好的理解,由於在js中基本類型的數據類型都存儲在棧空間。若是一個變量向另外一個變量賦值基本類型的值,會在變量對象上建立一個新值,而後把該值複製到爲新變量分配的位置上。
那麼爲何obj1和obj2的name輸出的結果都改變了呢?這是由於在js中引用類型的值保存在堆中的。若是一個變量向另外一個變量賦值引用類型的值,一樣會在變量對象上建立一個新值,而後把該值複製到爲新變量分配的位置上,但與基礎類型不一樣的是,這個值是一個指針,這個指針指向了堆中的同一個對象,所以在修改其中任何一個對象都是在對同一個對象修改。
看完上面的內容,相信你對棧和堆已經有了必定的理解,接下來咱們來看看js中傳遞參數的方式。在js中,全部函數的參數都是按值傳遞的,也就是說把函數外部的值複製給函數內部使用,就像把值從一個變量複製到另外一個變量裏同樣。
這就意味着,基本類型值得傳遞和引用類型值的傳遞就如同上述所說的複製過程是同樣的。
// 基本類型的傳遞
function addTen(num){
num += 10
return num
}
var count = 20
var result = addTen(count)
alert(count) //20
alert(result) //30
// 引用類型的傳遞
function setName(obj) {
obj.name = "小豬皮皮呆"
}
var person = {}
setName(person)
console.log(person.name) // 小豬皮皮呆
複製代碼
在這裏有些同窗可能會將引用類型傳遞參數的方式搞錯,會發出疑問:訪問變量有按值和按引用兩種方式,爲何傳遞參數只有按值傳遞?
對於上例的基礎類型的值的傳遞能夠很容易的理解,可是引用類型的傳遞在局部中的修改會在全局中反應出來,會有同窗誤覺得引用類型的傳遞是按參數傳遞的。但其實真正的過程是這樣的:
從上述的過程當中,能夠看出來,person這個變量是按值傳遞的。咱們再看個例子來講明這個問題
function setName(obj){
obj.name = "小豬皮皮呆"
obj = new Object()
obj.name = "三元大神"
}
var person = {}
setName(person)
alert(person.name) // 小豬皮皮呆
複製代碼
若是是按引用傳遞,顯示的值應該是「三元大神」,但js中的引用類型的傳遞也是按值傳遞的,因此打印出來的是「小豬皮皮呆」。
看到這裏,確定不少人要開始罵了,這我的標題黨啊,開頭說了要理清楚執行上下文、做用域、閉包、變量提高、this這些東西,怎麼到如今還隻字未提。都已經看到這裏了,彆着急!本文的思路是自頂向下的,從最外層你熟悉的地方開始講起,慢慢的滲透到底部的各個概念,將各個知識點串在一塊兒,造成知識體系。
showName() // 小豬
console.log(myName) // undefiend
var myName = "小豬皮皮呆"
function showName() {
console.log("小豬")
}
複製代碼
上面代碼的執行結果相信你們都不意外,這就是咱們耳熟能詳的變量提高,可是他的內部到底發生了些什麼,纔會出現這種結果呢?
不少地方給出的解釋是js代碼在執行的過程當中,js引擎會把變量的聲明部分和函數的聲明提高到代碼的開頭部分。變量被提高後會設置默認值,也就是undefined。 這種說法沒有錯,可是咱們要更深刻的去看看這個所謂的變量提高內部發生了什麼。
接下來咱們要開始咱們便進入了本文的重點部分,js代碼的執行流程分爲兩部分:編譯和執行。
看到這裏,終於迎來了咱們要討論的第一個重點:什麼是執行上下文?
執行上下文的建立分爲三種狀況:
而在js中,上下文的管理則由調用棧負責,js執行過程當中三種內存空間之一的棧空間。咱們來看看它是如何負責的:
看到這裏咱們即可以回答以前的問題了。所謂的變量提高就是js代碼執行的過程當中,會先將代碼進行編譯,編譯的過程當中變量的聲明和函數的聲明會被放入調用棧中造成上下文調用棧,剩餘下的會生成執行代碼。這就形成了變量提高的現象。
順帶一提,調用棧的大小有限,若是入棧執行的上下文超過必定數目,js引擎就會報錯,這種現象就叫棧溢出,看下面一段代碼:
function stackOverFlow (a, b) {
return stackOverFlow (a, b)
}
console.log(stackOverFlow(1, 2))
複製代碼
看到這裏,相信你已經理解了什麼是執行上下文,什麼是變量提高。是否是很簡單呢?接下來我會帶領同窗們繼續看剩下的幾個概念,有了上面的基礎,剩下的內容則更好理解。
在上文中咱們已經瞭解了變量提高,因爲 js 存在變量提高這種特性,從而致使了不少與直覺不符的代碼,這也是 JavaScript 的一個重要設計缺陷。
var name = "小豬皮皮呆"
function showName(){
console.log(name);
if (0) {
var name = "小豬"
}
console.log(name)
}
showName()
// undefined
// undefiend
複製代碼
在咱們熟悉調用棧後,在執行到showName時,會生成一個showName()的上下文,裏面會將函數內部的name放入變量環境中並賦值undefined,因此第一個console沒有打印出「小豬皮皮呆」,第二個打印以前由於if語句裏面的語句沒有執行,因此打印出的依然是undefined。
(1)做用域
而爲何會存在這種特性還得從做用域提及,js中存在三種做用域,ES6以前只兩種做用域:
(2)做用域鏈
這段代碼很容易讓人以爲會打印結果會是「小豬皮皮呆」,這和咱們接下來要提到的另外一個概念做用域鏈有關
function bar() {
console.log(name)
}
function foo() {
var name = "小豬皮皮呆"
bar()
}
var name = "小豬"
foo() // 小豬
複製代碼
相信前面的執行上下文部分同窗們已經理解了,接下來咱們會結合執行上下文來看做用域鏈:
(3)塊級做用域
上面提到了ES5以前只有全局做用域和函數做用域,ES6爲了解決變量提高帶來的問題,引入了塊級做用域。這個你們都很熟悉,可是js如何作到即支持變量提高的特性又支持塊級做用域呢?
咱們繼續從執行上下文的角度解決這個問題
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
複製代碼
上述造成的新的做用域鏈即是js對變量提高和塊級做用域同時支持的實現。
一個常見的問題:如何解決下面的循環輸出問題?
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
複製代碼
for(let i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
複製代碼
在瞭解了做用域鏈後再去理解閉包就十分簡單了!
ES5中存在兩個做用域:全局做用域、函數做用域,函數做用域會在函數運行結束後自動銷燬 做用域鏈:查找一個變量時會從自身的做用域開始沿着做用域鏈一直向上查找 閉包:利用了做用域,能夠將函數內部的做用域的變量訪問到
(1)閉包如何產生:
const a = 2
function out () {
let a = 1
return function b () {
console.log(a)
}
}
const b = out()
b() // 1
複製代碼
var a = 1
function bar(fn) {
var a = 2
console.log(fn)
}
function baz() {
console.log(a)
}
bar(baz) // 1
複製代碼
// 定時器
setTimeout(function timeHandler(){
console.log('111');
}, 100)
// 事件監聽
$('#app').click(function(){
console.log('DOM Listener');
})
複製代碼
var a = 2;
(function IIFE(){
// 輸出2
console.log(a);
})();
複製代碼
IIFE(當即執行函數表達式)建立閉包, 保存了全局做用域window和當前函數的做用域,所以能夠全局的變量。
for(var i = 1; i <= 5; i ++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
複製代碼
(2)應用場景:
函數柯里化、前端經典面試題解密-add(1)(2)(3)(4) == 10究竟是個啥?
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying(fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [...args, ...newArgs]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3, 4)(5, 6 ,7)()
console.log(total) // 28
複製代碼
(3)缺點:全局使用閉包會形成內存泄漏,因此儘可能少用
在上面一小節中咱們介紹了bar編譯好了後outer就指向全局上下文,所以打印的不是foo()內部的「小豬皮皮呆」,大多數人會產生這樣的異或即是將this和做用域鏈的概念弄混了。
而真實狀況是,做用域鏈這套機制不支持咱們直接得到對象內部的變量,而又獨立的成立了一套新的機制,絕對不要將二者混爲一談!
var obj = {
name: "小豬皮皮呆",
showName: function () {
console.log(name)
}
}
var name = "小豬"
obj.showName() // 小豬
複製代碼
上面是一個經典的面試題,輸出的結果是「小豬」而不是內部的「小豬皮皮呆」,有了以前對上下文和做用域鏈的理解,能夠很容易的去解釋,不在此贅述。
再強調一遍:做用域和this之間沒有任何關係!this單獨存在於執行上下文中,和執行上下文中的變量環境、詞法環境、outer是並行的關係。
那麼this要如何使用呢?若是想上述代碼輸出內部的name,即可以使用this來實現。
var obj = {
name: "小豬皮皮呆",
showName: function () {
console.log(this.name)
}
}
var name = "小豬"
obj.showName() // 小豬皮皮呆
複製代碼
接下來再對this的指向作一個總結:
var person = {
name: "小豬皮皮呆",
changeName: function () {
setTimeout(function(){
this.name = "小豬"
}, 100)
}
}
person.changeName()
複製代碼
上述代碼想要經過changeName方法修改person內部的name屬性,可是該代碼存在一些問題,咱們便根據上述對this指向的總結來解決這題。
(1) 緩存內部的this
var person = {
name: "小豬皮皮呆",
changeName: function () {
var self = this
setTimeout(function(){
self.name = "小豬"
console.log(person.name)
}, 100)
}
}
person.changeName() // 小豬
複製代碼
(2) 使用call、apply或bind顯示綁定
var change = function () {
this.name = "小豬"
}
var person = {
name: "小豬皮皮呆",
changeName: function () {
setTimeout(function(){
change.call(person)
console.log(person.name)
}, 100)
}
}
person.changeName() // 小豬
複製代碼
(3) 使用箭頭函數
var person = {
name: "小豬皮皮呆",
changeName: function () {
setTimeout(() => {
this.name = "小豬"
console.log(person.name)
}, 100)
}
}
person.changeName() // 小豬
複製代碼
好啦,到此爲止文章開頭提到的那幾個詞已經爲你們梳理過一遍了,若是以爲還不錯的話,給小豬皮皮呆一個👍吧!
參考文獻
- 《js高級程序設計》第三版
- 極客的專欄
- 《你不知道的js》
- 偶像神三元的博客