JavaScript 是一種弱類型腳本語言,所謂弱類型指的是定義變量時,不須要什麼類型,在程序運行過程當中會自動判斷類型。html
ECMAScript 中定義了 6 種原始類型:前端
注意:原始類型不包含 Object。面試
第一問:類型判斷用到哪些方法?數組
typeof
typeof xxx
獲得的值有如下幾種類型:undefined、
boolean、
number、s
tring、
object、
function
、symbol
,比較簡單。這裏須要注意的有三點:瀏覽器
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
第二問:值類型和引用類型的區別app
除了原始類型,ES 還有引用類型,上文提到的typeof
識別出來的類型中,只有object
和function
是引用類型,其餘都是值類型。函數
根據 JavaScript 中的變量類型傳遞方式,又分爲值類型和引用類型,值類型變量包括 Boolean、String、Number、Undefined、Null,引用類型包括了 Object 類的全部,如 Date、Array、Function 等。在參數傳遞方式上,值類型是按值傳遞,引用類型是按共享傳遞。測試
面經過一個小題目,來看下二者的主要區別,以及實際開發中須要注意的地方。this
// 值類型
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
的也跟着變化。由於a
和b
都是引用類型,指向了同一個內存地址,即二者引用的是同一個值,所以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
自己是個引用類型的變量(對象),可是內部的a
和b
一個是值類型一個是引用類型,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__
就是一個普通的對象而已嘛!
這樣一直往上找,你會發現是一個鏈式的結構,因此叫作「原型鏈」。若是一直找到最上層都沒有找到,那麼就宣告失敗,返回undefined
。最上層是什麼 —— Object.prototype.__proto__ === null
this
全部從原型或更高級原型中獲得、執行的方法,其中的this
在執行時,就指向了當前這個觸發事件執行的對象。所以printName
和alertName
中的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,函數則先聲明好可以使用。這一步作完了,而後再開始正式執行程序。再次強調,這是在代碼執行以前纔開始的工做。
咱們來看下上面的面試小題目,爲何a是undefined,而b卻報錯了,實際 JS 在代碼執行以前,要「全文解析」,發現var a,知道有個a的變量,存入了執行上下文,而b沒有找到var關鍵字,這時候沒有在執行上下文提早「佔位」,因此代碼執行的時候,提早報到的a是有記錄的,只不過值暫時尚未賦值,即爲undefined,而b在執行上下文沒有找到,天然會報錯(沒有找到b的引用)。
另外,一個函數在執行以前,也會建立一個 函數執行上下文 環境,跟 全局上下文 差很少,不過函數執行上下文 中會多出this
arguments
和函數的參數。參數和arguments
好理解,這裏的this
我們須要專門講解。
總結一下:
<script>
、js 文件或者一個函數this
,arguments。
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}
這就是爲什麼 jQuery、Zepto 等庫的源碼,全部的代碼都會放在(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
的代碼再看閉包,就很好理解了。