通過一道面試題來學習原型/原型鏈-函式宣告/函式表示式

不吃早餐發表於2018-07-19

例題

如題:

通過一道面試題來學習原型/原型鏈-函式宣告/函式表示式

答案解析

function A() {
    B = function () {console.log(10)}
    return this
};

A.B = function () {console.log(20)};

A.prototype.B = function () {console.log(30)}

var B = function () {console.log(40)}

function B() {console.log(50)}

A.B() // answer 20 【原型與原型鏈】
// 在`A`的原型物件中查詢是否有`B`函式並且呼叫,這裡並未執行`A`函式。
// A.B = function () {console.log(20)};
// 在A的原型物件中新增了`B`函式,停止查詢,所以答案為 20

B() // answer 40 【函式表示式和函式宣告】
// var B = function () {console.log(40)}
// function B() {console.log(50)}
// 同名 -> 函式提升會 > 變數提升
// 換言之 同名的函式表示式和函式宣告同時存在時 總是執行表示式

A().B() // answer 10 【函式表示式和函式宣告】
// A() 執行函式A ==> 1.變數B重新賦值函式 2.返回this(window)
// .B() 執行全域性下的B函式 已經被重新賦值 所以輸出10

B() // answer 10 
// 上面的程式碼執行過A函式了,此時全域性下的B函式輸出10

new A.B() // answer 20【函式表示式和函式宣告】
// new 執行了 A.B = function () {console.log(20)};

new A().B() // answer 30
// new 執行建構函式 A ,全域性變數 B 重新賦值函式10
// 此時A() 指標指向哪裡? 
// 首先要知道 new 做了什麼事?
// ==> 建立空物件objA objA.__proto__ = A.prototype
// .B() 在A的原型物件中查詢 B; A.prototype 指向函式的原型物件
// A.prototype.B = function () {console.log(30)} 輸出 30
複製程式碼

原型和原型鏈

prototype

每一個函式都有一個 prototype 屬性。

function Foo() {}

Foo.prototype; // {constructor,__proto__}
複製程式碼

無論什麼時候,只要建立了一個新函式,根據一組特定的規則為該函式建立一個prototype 屬性,這個屬性指向函式的原型物件。

那麼這個建立的原型物件是什麼呢?

{
    constructor: ƒ Foo(),
    __proto__: Object
}
複製程式碼

constructor 屬性

每一個原型物件都有一個 constructor 屬性

建立了自定義的建構函式後,其原型物件只會預設取得 constructor 屬性。這個屬性解決了物件識別問題,即可以通過該屬性判斷出例項是由哪個建構函式建立的。

Foo.prototype.constructor === Foo; //  true
複製程式碼

前面說了,原型物件只會預設取得 constructor 屬性,那麼原型物件的其他屬性(比如:__proto__ )是這麼來的呢,這就要說到 __proto__ 指標了。

proto

每一個例項都有一個 __proto__ 指標,指向建構函式的原型物件。

var foo = new Foo();
foo.__proto__ === Foo.prototype; //true
複製程式碼

上面提到的建構函式的原型物件它本身也是一個例項,所以在它內部會有一個__proto__ 指標。

建構函式(補充)

ECMAScript 中提供了建構函式來建立新物件。但建構函式本身就是一個函式,與普通函式沒有任何區別,只不過為了區分,一般將其首字母大寫,但這並不是必須的。

函式被 new 關鍵字呼叫時就是建構函式。

function f(name) {
    console.log("execute");
    this.name = name;
}

var k = new f("k"); // execute =====> 呼叫new
console.log(k); // {name: "k"}
var h = f("h"); // execute  =====> 未呼叫new
console.log(h); // undefined
複製程式碼

從上面程式碼可以看出:

  • 首字母是否大寫並不影響函式 f 作為建構函式使用,
  • 不使用 new 呼叫函式就是普通函式,直接執行內部程式碼,使用new,函式的角色就成為了建構函式,建立一個物件並返回。

物件由建構函式通過 new 創造物件的步驟

var obj = {}; // 建立一個空物件
obj.__proto__ = constructor.prototype;//新增__proto__屬性,並指向建構函式的prototype 屬性。
constructor.call(this); // 繫結this
return obj;
複製程式碼

new 關鍵字的內部實現機制:

  • 建立一個新物件;
  • 將建構函式的作用域賦值給新物件;
  • 執行建構函式中的程式碼;
  • 返回新物件

原型鏈

原型鏈的理論主要基於上述提到的建構函式、例項和原型的關係:

  • 每一個建構函式都有一個原型物件
  • 原型物件都包含一個指向建構函式的 constructor 屬性
  • 每一個例項都包含一個指向原型物件的 __proto__ 指標 其中最最重要的是第三條,依賴這條關係,層層遞進,就形成了例項與原型的鏈條。

接著上面的探索,建構函式的原型的原型是由 Object 生成的,那麼 Object 的原型是由什麼生成?而原型鏈的終點又是在哪?

Object.prototype.__proto__ // null
null.__proto__; // Uncaught TypeError: Cannot read property '__proto__' of null
// game over
複製程式碼

原型的終點是 null,因為 null 沒有 proto 屬性。

最後以一個例子來理解上面所談到的原型與原型鏈

function Foo(){} // 建構函式 Foo
var foo = new Foo() // foo.__proto__ 指向 Foo.prototype
複製程式碼

通過一道面試題來學習原型/原型鏈-函式宣告/函式表示式

函式宣告、函式表示式

函式宣告 function name(){}

函式宣告是用指定的引數宣告一個函式。一個被函式宣告建立的函式是一個 Function 物件,具有 Function 物件的所有屬性、方法和行為。

// 函式宣告語法
function name([param[, param[, ... param]]]) { statements }
複製程式碼

函式表示式 var name = function(){}

在函式表示式中我們可以忽略函式名稱建立匿名函式,並將該匿名函式賦值給變數。

var add = function(a, b) {
    return a + b;  
};

add(2, 3) // 5
複製程式碼

當然, 也可以建立命名函式表示式 Named function expression:

var add = function func(a, b) {
    return a + b;  
};

add(2, 3) // 5
複製程式碼

命名函式表示式中函式名稱只能作為函式體作用域內的區域性變數,外部不可訪問。

var a = function pp(v) {
    v++;
    if (v>3) {
        return v;
    } else {
        return pp(v);
    }
}

a(1); // 4
pp(1); // ReferenceError: pp is not defined
複製程式碼

函式宣告提升

對於函式宣告建立的函式,可以在本作用域內任意位置訪問。

a(); // 1

function a() {
    return 1;  
}

a(); // 1
複製程式碼

而函式表示式不會。

console.log(a); // undefined (只是變數提升)
a(1); // TypeError: a is not a function

var a = function(v) {
    console.log(v);     
};
複製程式碼

函式提升和變數提升的疑惑分析

console.log(fn); // [Function: fn]
var fn = function () {
    console.log(1);
}

function fn() {
    console.log(2);
}

fn() // 1
複製程式碼

提升過程

// 函式提升
function fn() {
    console.log(2);
}

// 變數提升
var fn;

fn = function () {
    console.log(1);
}
fn() //最終輸出1
複製程式碼

參考


相關文章