前端綜合學習筆記---變量類型、原型鏈、作用域和閉包


個人bolg地址

變量類型

  JavaScript 是一種弱類型腳本語言,所謂弱類型指的是定義變量時,不需要什么類型,在程序運行過程中會自動判斷類型。

ECMAScript 中定義了 6 種原始類型:

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol(ES6 新定義)

注意:原始類型不包含 Object。

第一問:類型判斷用到哪些方法?

typeof

typeof xxx得到的值有以下幾種類型:undefined、boolean、number、string、object、functionsymbol ,比較簡單。這里需要注意的有三點:

  1. typeof null結果是object ,實際這是typeof的一個bug,null是原始值,非引用類型
  2. typeof [1, 2]結果是object,結果中沒有array這一項,引用類型除了function其他的全部都是object 
  3. typeof Symbol()typeof獲取symbol類型的值得到的是symbol,這是 ES6 新增的知識點

instanceof

用於實例和構造函數的對應。例如判斷一個變量是否是數組,使用typeof無法判斷,但可以使用[1, 2] instanceof Array來判斷。因為,[1, 2]是數組,它的構造函數就是Array。同理:

function Foo(name) { this.name = name } var foo = new Foo('bar') console.log(foo instanceof Foo) // true

第二問:值類型和引用類型的區別

  值類型 vs 引用類型

 除了原始類型,ES 還有引用類型,上文提到的typeof識別出來的類型中,只有objectfunction是引用類型,其他都是值類型。

根據 JavaScript 中的變量類型傳遞方式,又分為值類型和引用類型,值類型變量包括 Boolean、String、Number、Undefined、Null,引用類型包括了 Object 類的所有,如 Date、Array、Function 等。在參數傳遞方式上,值類型是按值傳遞,引用類型是按共享傳遞。

面通過一個小題目,來看下兩者的主要區別,以及實際開發中需要注意的地方。

// 值類型
var a = 10
var b = a b = 20 console.log(a) // 10
console.log(b)  // 20

上述代碼中,a b都是值類型,兩者分別修改賦值,相互之間沒有任何影響。再看引用類型的例子

// 引用類型
var a = {x: 10, y: 20} var b = a b.x = 100 b.y = 200 console.log(a) // {x: 100, y: 200}
console.log(b)  // {x: 100, y: 200}

上述代碼中,a b都是引用類型。在執行了b = a之后,修改b的屬性值,a的也跟着變化。因為ab都是引用類型,指向了同一個內存地址,即兩者引用的是同一個值,因此b修改屬性時,a的值隨之改動。

再借助題目進一步講解一下

說出下面代碼的執行結果,並分析其原因。

function foo(a){ a = a * 10; } function bar(b){ b.value = 'new'; } var a = 1; var b = {value: 'old'}; foo(a); bar(b); console.log(a); // 1
console.log(b); // value: new

通過代碼執行,會發現:

  • a的值沒有發生改變
  • b的值發生了改變

這就是因為Number類型的a是按值傳遞的,而Object類型的b是按共享傳遞的。

JS 中這種設計的原因是:按值傳遞的類型,復制一份存入棧內存,這類類型一般不占用太多內存,而且按值傳遞保證了其訪問速度。按共享傳遞的類型,是復制其引用,而不是整個復制其值(C 語言中的指針),保證過大的對象等不會因為不停復制內容而造成內存的浪費。

引用類型經常會在代碼中按照下面的寫法使用,或者說容易不知不覺中造成錯誤!

var obj = { a: 1, b: [1,2,3] } var a = obj.a var b = obj.b a = 2 b.push(4) console.log(obj, a, b)//{a:1,b:[1,2,3,4]},2,[1,2,3,4]

雖然obj本身是個引用類型的變量(對象),但是內部的ab一個是值類型一個是引用類型,a的賦值不會改變obj.a,但是b的操作卻會反映到obj對象上。

原型和原型鏈

JavaScript 是基於原型的語言,原型理解起來非常簡單,但卻特別重要,下面還是通過題目來理解下JavaScript 的原型概念。

第三問:如何理解 JavaScript 的原型

對於這個問題,可以從下面這幾個要點來理解和回答,下面幾條必須記住並且理解

  • 所有的引用類型(數組、對象、函數),都具有對象特性,即可自由擴展屬性(null除外)
  • 所有的引用類型(數組、對象、函數),都有一個__proto__屬性,屬性值是一個普通的對象
  • 所有的的函數,都有一個prototype屬性,屬性值也是一個普通的對象
  • 所有的的引用類型(數組、對象、函數),__proto__屬性值指向它的構造函數的prototype屬性值

通過代碼解釋一下,大家可自行運行以下代碼,看結果

// 要點一:自由擴展屬性
var obj = {}; obj.a = 100; var arr = []; arr.a = 100; function fn () {} fn.a = 100; // 要點二:__proto__
console.log(obj.__proto__); console.log(arr.__proto__); console.log(fn.__proto__); // 要點三:函數有 prototype
console.log(fn.prototype) // 要點四:引用類型的 __proto__ 屬性值指向它的構造函數的 prototype 屬性值
console.log(obj.__proto__ === Object.prototype)

原型

先寫一個簡單的代碼示例。

// 構造函數
function Foo(name, age) {
    this.name = name
}
Foo.prototype.alertName = function () {
    alert(this.name)
}
// 創建示例
var f = new Foo('zhangsan')
f.printName = function () {
    console.log(this.name)
}
// 測試
f.printName()
f.alertName()

  執行printName時很好理解,但是執行alertName時發生了什么?這里再記住一個重點 當試圖得到一個對象的某個屬性時,如果這個對象本身沒有這個屬性,那么會去它的__proto__(即它的構造函數的prototype)中尋找,因此f.alertName就會找到Foo.prototype.alertName

那么如何判斷這個屬性是不是對象本身的屬性呢?使用hasOwnProperty,常用的地方是遍歷一個對象的時候。

var item
for (item in f) {
    // 高級瀏覽器已經在 for in 中屏蔽了來自原型的屬性,但是這里建議大家還是加上這個判斷,保證程序的健壯性
    if (f.hasOwnProperty(item)) {
        console.log(item)
    }
}

 第四問:如何理解 JS 的原型鏈

還是接着上面的示例,如果執行f.toString()時,又發生了什么?

 

// 省略 N 行

// 測試
f.printName() f.alertName() f.toString()

 

因為f本身沒有toString(),並且f.__proto__(即Foo.prototype)中也沒有toString。這個問題還是得拿出剛才那句話——當試圖得到一個對象的某個屬性時,如果這個對象本身沒有這個屬性,那么會去它的__proto__(即它的構造函數的prototype)中尋找。

如果在f.__proto__中沒有找到toString,那么就繼續去f.__proto__.__proto__中尋找,因為f.__proto__就是一個普通的對象而已嘛!

  1. f.__proto__Foo.prototype,沒有找到toString,繼續往上找
  2. f.__proto__.__proto__即Foo.prototype.__proto__Foo.prototype就是一個普通的對象,因此Foo.prototype.__proto__就是Object.prototype,在這里可以找到toString
  3. 因此f.toString最終對應到了Object.prototype.toString

這樣一直往上找,你會發現是一個鏈式的結構,所以叫做“原型鏈”。如果一直找到最上層都沒有找到,那么就宣告失敗,返回undefined。最上層是什么 —— Object.prototype.__proto__ === null

原型鏈中的this

 所有從原型或更高級原型中得到、執行的方法,其中的this在執行時,就指向了當前這個觸發事件執行的對象。因此printNamealertName中的this都是f

作用域和閉包

作用域和閉包是前端面試中,最可能考查的知識點。例如下面的題目:

第五問:現在有個 HTML 片段,要求編寫代碼,點擊編號為幾的鏈接就alert彈出其編號;

<ul>
    <li>編號1,點擊我請彈出1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

一般不知道這個題目用閉包的話,會寫出下面的代碼

var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(){ alert(i + 1) }, true) }

實際上執行才會發現始終彈出的是6,這時候就應該通過閉包來解決:

var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(i){ return function(){ alert(i + 1) } }(i), true) }

要理解閉包,就需要我們從「執行上下文」開始講起。

執行上下文

這個在我另一篇文章里講過  點擊鏈接

先講一個關於 變量提升 的知識點,面試中可能會遇見下面的問題,很多候選人都回答錯誤:

  第六問:說出下面執行的結果(這里我就直接注釋輸出了)

console.log(a)  // undefined
var a = 100 fn('zhangsan')  // 'zhangsan' 20
function fn(name) { age = 20 console.log(name, age) var age } console.log(b); // 這里報錯 // Uncaught ReferenceError: b is not defined
b = 100;

在一段 JS 腳本(即一個<script>標簽中)執行之前,要先解析代碼(所以說 JS 是解釋執行的腳本語言),解析的時候會先創建一個 全局執行上下文 環境,先把代碼中即將執行的(內部函數的不算,因為你不知道函數何時執行)變量、函數聲明都拿出來。變量先暫時賦值為undefined,函數則先聲明好可使用。這一步做完了,然后再開始正式執行程序。再次強調,這是在代碼執行之前才開始的工作。

我們來看下上面的面試小題目,為什么aundefined,而b卻報錯了,實際 JS 在代碼執行之前,要「全文解析」,發現var a,知道有個a的變量,存入了執行上下文,而b沒有找到var關鍵字,這時候沒有在執行上下文提前「占位」,所以代碼執行的時候,提前報到的a是有記錄的,只不過值暫時還沒有賦值,即為undefined,而b在執行上下文沒有找到,自然會報錯(沒有找到b的引用)。

另外,一個函數在執行之前,也會創建一個 函數執行上下文 環境,跟 全局上下文 差不多,不過函數執行上下文 中會多出this arguments和函數的參數。參數和arguments好理解,這里的this咱們需要專門講解。

總結一下:

  • 范圍:一段<script>js 文件或者一個函數
  • 全局上下文:變量定義,函數聲明
  • 函數上下文:變量定義,函數聲明,thisarguments。

this

先搞明白一個很重要的概念 —— this的值是在執行的時候才能確認,定義的時候不能確認! 為什么呢 —— 因為this是執行上下文環境的一部分,而執行上下文需要在代碼執行之前確定,而不是定義的時候。看如下例子:

var a = { name: 'A', fn: function () { console.log(this.name) } } a.fn() // this === a
a.fn.call({name: 'B'})  // this === {name: 'B'}
var fn1 = a.fn fn1() // this === window

this執行會有不同,主要集中在這幾個場景中

  • 作為構造函數執行,構造函數中
  • 作為對象屬性執行,上述代碼中a.fn()
  • 作為普通函數執行,上述代碼中fn1()
  • 用於call apply bind,上述代碼中a.fn.call({name: 'B'})

下面再來講解下什么是作用域和作用域鏈,作用域鏈和作用域也是常考的題目。

第七問:如何理解 JS 的作用域和作用域鏈

作用域

ES6 之前 JS 沒有塊級作用域。例如

if (true) { var name = 'zhangsan' } console.log(name)

從上面的例子可以體會到作用域的概念,作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去。上面的name就被暴露出去了,因此,JS 沒有塊級作用域,只有全局作用域函數作用域

var a = 100
function fn() { var a = 200 console.log('fn', a) } console.log('global', a) fn()

全局作用域就是最外層的作用域,如果我們寫了很多行 JS 代碼,變量定義都沒有用函數包括,那么它們就全部都在全局作用域中。這樣的壞處就是很容易撞車、沖突。

// 張三寫的代碼中
var data = {a: 100} // 李四寫的代碼中
var data = {x: true}

這就是為何 jQueryZepto 等庫的源碼,所有的代碼都會放在(function(){....})())中。因為放在里面的所有變量,都不會被外泄和暴露,不會污染到外面,不會對其他的庫或者 JS 腳本造成影響。這是函數作用域的一個體現。

附:ES6 中開始加入了塊級作用域,使用let定義變量即可,如下:

if (true) { let name = 'zhangsan' } console.log(name) // 報錯,因為let定義的name是在if這個塊級作用域

作用域鏈

首先認識一下什么叫做 自由變量 。如下代碼中,console.log(a)要得到a變量,但是在當前的作用域中沒有定義a(可對比一下b)。當前作用域沒有定義的變量,這成為 自由變量 。自由變量如何得到 —— 向父級作用域尋找。

var a = 100
function fn() { var b = 200 console.log(a) console.log(b) } fn()

如果父級也沒呢?再一層一層向上尋找,直到找到全局作用域還是沒找到,就宣布放棄。這種一層一層的關系,就是 作用域鏈 。

var a = 100
function F1() { var b = 200
    function F2() { var c = 300 console.log(a) // 自由變量,順作用域鏈向父作用域找
        console.log(b) // 自由變量,順作用域鏈向父作用域找
        console.log(c) // 本作用域的變量
 } F2() } F1()

閉包

講完這些內容,我們再來看一個例子,通過例子來理解閉包。

function F1() { var a = 100
    return function () { console.log(a) } } var f1 = F1() var a = 200 f1()  //100

自由變量將從作用域鏈中去尋找,但是 依據的是函數定義時的作用域鏈,而不是函數執行時,以上這個例子就是閉包。閉包主要有兩個應用場景:

  • 函數作為返回值,上面的例子就是
  • 函數作為參數傳遞,看以下例子
function F1() { var a = 100
    return function () { console.log(a) } } function F2(f1) { var a = 200 console.log(f1()) }
var f1 = F1() F2(f1) //100

至此,對應着「作用域和閉包」這部分一開始的點擊彈出alert的代碼再看閉包,就很好理解了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM