上一篇我們講到了,在前端面試的時候常被問到的函式及函式作用域的問題。今天這篇我們講js的一個比較重要的甚至在程式設計的世界都很重要的問題 物件導向 。
在JavaScript中一切都是物件嗎?
“一切皆物件!” 大家都對此深信不疑。其實不然,這裡面帶有很多的語言陷阱,還是不要到處給別人吹噓一切皆物件為好。
資料型別
JavaScript 是一種弱型別或者說動態語言。這意味著你不用提前宣告變數的型別,在程式執行過程中,型別會被自動確定。這也意味著你可以使用同一個變數儲存不同型別的資料,最新的 ECMAScript 標準定義了 7 種資料型別:
基本型別
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ECMAScript 6 新定義)
物件型別
- Object
物件型別涵蓋了很多引用型別,任何非基本型別的都是物件型別。如Function(函式Function 是一個附帶可被呼叫功能的常規物件),這裡就不在贅述。
根據這個分類可以看出“並非一切接物件”。
我們可以從兩方面來區別這兩種型別:
區別
可變性
基本型別:不可變型別,無法新增屬性;即使新增屬性,解析器無法再下一步讀取它;
var cat = "cat";
cat.color = "black";
cat.color // undefined
複製程式碼
物件型別:可變型別,支援新增和刪除屬性。
比較和傳遞
基本型別:按值比較,按值傳遞;
物件型別:按引用比較,按引用傳遞。
// 基本型別
var cat = "tom";
var dog = "tom";
cat === dog // true
//物件型別
var cat = {name:"tom"};
var dog = {name:"tom"};
cat === dog //false
複製程式碼
我們說的通過引用進行物件比較是:兩個物件的值是否相同取決於它們是否指向相同的底層物件 __David Flanagan
所以我們改成這樣:
var cat = {name:"tom"}
var dog = cat;
b.name = "Haba"
dog === cat // true
複製程式碼
如何檢測物件型別?或者怎麼檢測一個資料是陣列型別?
檢測一個物件的型別,強烈推薦使用 Object.prototype.toString 方法; 因為這是唯一一個可依賴的方式。 我們使用Object.prototype.toString方法:
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(2) // "[object Number]"
複製程式碼
為什麼不能用typeOf ?
typeof只有在基本型別的檢測上面才好使,在引用型別(Function除外)裡面他返回的都是object,另 typeof null === "object".
簡談物件導向
“物件導向程式設計(OOP)” 是目前主流的程式設計正規化,其核心思想是將真實世界中各種複雜的關係,抽象為一個個物件,然後由物件之間的分工與合作,完成對真實世界的模擬。
物件是單個實物的抽象,一本書,一隻貓,一個人都可以是物件。從語言的角度物件就是一個容器封裝了屬性和方法。
典型物件導向的兩大概念:類和 例項
- 類:物件的型別模板
- 例項:根據類建立的物件
很遺憾JavaScript沒有類的概念,但它使用建構函式(constructor)作為物件的模板。
//建構函式
var Pet = function (name, language) {
this.name = name;
this.say = function () {
console.log(language);
}
}
// new 關鍵字生成物件 有關new操作符我們在後面會講到。
var cat = new Pet('tom', 'meow');
cat.name // tom
cat.say() // meow
複製程式碼
new建立一個物件都進行了哪些操作?
new用於新建一個物件,例如:
function Pet () {}
var tom = new Pet();
複製程式碼
new進行了如下操作:
- 建立一個空物件,用this 變數引用該物件並繼承該函式的原型
- 屬性和方法加入到this的引用物件中
- 新建立的物件由this所引用,並且最後隱式的返回this
模擬過程:function newObj(Fun,arguments) { var o = {}; if (Fun && typeof Fun === "function") { o.__proto__ = Fun.prototype; Fun.apply(o, arguments); return o; } } 複製程式碼
這裡需要注意的是,建構函式內部有return語句的情況。如果return 後面跟著一個物件,new命令返回return指定的物件;否則不管return語句直接返回this.
var Pet = function (name) {
this.name = name;
return {notInstance:"blabla"}
}
var cat = new Pet('tom');
cat.name // undefined
cat.notInstance // blabla
複製程式碼
闡述原型鏈?js如何實現繼承?
上面的講到的建構函式,例項物件的屬性和方法都在建構函式內部實現。這樣的 建構函式有一個缺點:
var cat1 = new Pet('tom', 'meow');
var cat2 = new pet('jery', 'meow');
cat1.say === cat2.say // false
複製程式碼
生成兩隻貓 叫聲一樣,但是貓的say方法是不一樣的,就是說每新建一個物件就生成一個新的say方法。所有的say方法都是同樣的行為,完全可以共享。
JavaScript的原型(prototype)可以讓我們實現共享。
原型鏈 ?
JavaScrip可以採用構造器(constructor)生成一個新的物件,每個構造器都擁有一個prototype屬性,而每個通過此構造器生成的物件都有一個指向該構造器原型(prototype)的內部私有的連結(proto),而這個prototype因為是個物件,它也擁有自己的原型,這麼一級一級直到原型為null,這就構成了原型鏈.
原型鏈的工作原理:
function getProperty(obj, prop) {
if (obj.hasOwnProperty(prop)) //首先查詢自身屬性,如果有則直接返回
return obj[prop]
else if (obj.__proto__ !== null)
return getProperty(obj.__proto__, prop) //如何不是私有屬性,就在原型鏈上一步步向上查詢,直到找到,如果找不到就返回undefind
else
return undefined
}
複製程式碼
如果跟著原型鏈一層層的尋找,所有物件都可以尋找到最頂層,Object.prototype, 即Object的建構函式的prototype屬性,而Object.prototype物件指向的就是沒有任何屬性和方法的null物件。
Object.getPrototypeOf(Object.prototype)
// null
複製程式碼
原型連結串列明瞭一個物件查詢他的屬性的過程:首先在物件本身上面找 -> 沒找到再到物件的原型上找 ->還是找不到就到原型的原型上找 —>直到Object.prototype找不到 -> 返回undefined。(在這種查詢中找到就立刻返回)。
constructor 屬性
prototype物件有一個constructor屬性,預設指向prototype物件所在的建構函式。
由於constructor屬性是一種原型物件與建構函式的關聯關係,所以修改原型物件的時候,務必要小心。
var Pet = function (name) {
this.name = name;
}
// 儘量避免這麼寫,因為會把construct屬性覆蓋掉。
Pet.prototype = {
say: function () {
console.log('meow');
}
}
// 如果我們覆蓋了constructor屬性要記得將他指回來。
Pet.prototype.constructor = Pet;
複製程式碼
__proto__ 屬性和prototype屬性的區別
prototype是function物件中專有的屬性。
__proto__是普通物件的隱式屬性,在new的時候,會指向prototype所指的物件;
__proto__實際上是某個實體物件的屬性,而prototype則是屬於建構函式的屬性。
__proto__只能在學習或除錯的環境下使用。
這裡抓住兩點:
- 建構函式通過 prototype 屬性訪問原型物件
- 例項物件通過 [[prototype]] 內部屬性訪問原型物件,瀏覽器實現了 __proto__屬性用於例項物件訪問原型物件
Object 為建構函式時,是Function的例項物件;Function為建構函式時,Function.prototype 是物件,那麼他就是Object的例項物件。
來看一個題目:
var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();
// f 能取到a,b嗎?原理是什麼?
複製程式碼
根據原型鏈的關係:
f是F的例項物件,其原型鏈:
f.__proto__ -> [F prototype].__proto__ -> [Object prototype].__proto__ -> null
複製程式碼
F是建構函式,是Function的例項,他的原型鏈:
F.__proto__ -> [Function prototype].__proto__ -> [Object prototype].__proto__ -> null
複製程式碼
由此,只有F能夠訪問到Function的prototype,答案就是:“f只能a,但是F可以訪問a,b”
原型繼承
原型繼承是藉助已有的物件建立新的物件,將子類的原型指向父類,就相當於加入了父類這條原型鏈。
function Animal(){
this.super = 'animal';
}
function Cat(name) {
this.name = name;
this.food = 'fish';
}
Cat.prototype = new Animal(); // Cat 繼承了Animal
Cat.prototype.getFood = function () {
return this.food;
}
複製程式碼
上面的方法中constructor指向有點問題:
var cat = new Cat('tom');
cat.name // tom
cat.super // animal
cat.getFood() // fish
//but
cat.constructor === Cat //false
cat.constructor === Animal //true
複製程式碼
cat 的constructor 並沒有指向Cat而是指向了父類Animal。我們需要對它進行修正:
function Cat(name){
this.name = name;
this.food = 'fish';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.getFood = function () {
return this.food;
}
複製程式碼
好了上面就實現了一個簡單的原型繼承。
總結
js的靈活性造就了實現繼承的多樣性,或者說因為他沒有真正的類和繼承,我們可以利用很多中方式來模擬它。原型繼承是最有js特色的一直實現方式,也是使用最多的方式。關於物件導向,我想說js “幾乎一切皆物件”,因為有原型鏈的存在我們能實現類似其他語言的繼承。
加上前面一篇,兩篇文章已經涵蓋了js大半部分面試問題了,接下來的文章可能會講解一下單執行緒模型和計時器相關。這塊是個難點我也是看了好多資料後才搞明白了大概。這次的面試系列主要還是針對於“中高階前端”,也是一個進階的層次,各位看官不要灰心一切都會有撥雲見日的一天。
女排奪冠了,今年好像好多我關注的時間都有了完美的結局,很是為他們高興,也希望我的未來也是一個完美的結局,晚安。
請關注我的專欄 《前端雜貨鋪》