前端面試之js相關問題(二)

前端雜貨鋪發表於2018-01-11

上一篇我們講到了,在前端面試的時候常被問到的函式及函式作用域的問題。今天這篇我們講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大半部分面試問題了,接下來的文章可能會講解一下單執行緒模型和計時器相關。這塊是個難點我也是看了好多資料後才搞明白了大概。這次的面試系列主要還是針對於“中高階前端”,也是一個進階的層次,各位看官不要灰心一切都會有撥雲見日的一天。

女排奪冠了,今年好像好多我關注的時間都有了完美的結局,很是為他們高興,也希望我的未來也是一個完美的結局,晚安。

請關注我的專欄 《前端雜貨鋪》

參考文章


相關文章