1. 程式人生 > >JS入門難點解析11-建構函式,原型物件,例項物件

JS入門難點解析11-建構函式,原型物件,例項物件

(注1:如果有問題歡迎留言探討,一起學習!轉載請註明出處,喜歡可以點個贊哦!)
(注2:本文首發於我的簡書,更多內容請檢視我的目錄。)

1. 簡介

在前面,我們對這三個概念已經有所涉及,但是卻並未深究。事實上,如果能熟練理解掌握這三個概念和他們之間的關係,那麼在學習原型鏈和繼承的知識時,會有一種撥雲見霧之感。

2. 建構函式

建構函式其實與普通函式本身並無區別,普通函式通過new呼叫時,我們就稱其為建構函式。當然,為了區分其與普通函式,建構函式約定首字母需要大寫。下面,我們就來看一下建構函式和普通函式使用時的區別(簡單來講就是一個函式通過new呼叫和不通過new呼叫的區別)。

2.1 一個空函式

// 空函式
function A() {}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);  //undefined
console.log('a2:', a2);  //{}

在chrome的控制檯console執行結果如圖所示:
2.1
直接呼叫返回undefined,而使用new呼叫返回的卻是一個空物件。這裡,我們暫且不去討論__proto__和constructor的含義。

2.2 無this有return,但是return後面無返回值,或者返回基本型別值。

// 無返回值
function
A() {
return; } //返回undefined型別值 function B() { return undefined; } // 返回Number型別值 function C() { return 1; } // 返回String型別值 function D() { return '1'; } // 返回Boolean型別值 function E() { return true; } // 返回Null型別值 function F() { return null; } var a1 = A(); var a2 = new A(); console.log('a1:'
, a1); console.log('a2:', a2); var b1 = B(); var b2 = new A(); console.log('b1:', b1); console.log('b2:', b2); var c1 = C(); var c2 = new A(); console.log('c1:', c1); console.log('c2:', c2); var d1 = D(); var d2 = new A(); console.log('d1:', d1); console.log('d2:', d2); var e1 = E(); var e2 = new A(); console.log('e1:', e1); console.log('e2:', e2); var f1 = F(); var f2 = new A(); console.log('f1:', f1); console.log('f2:', f2);

2.2

可以看到,普通呼叫會返回return後面的值,而new呼叫返回空物件{}。

2.3 無this有return,但是return後面是一個物件(包括函式)。

// 返回物件
function A() {
    return {m: 1};
}
//返回函式
function B() {
    return function () {
        return 123;
    }
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);

2.3
可以看出,不管是普通呼叫還是new呼叫都是返回return後面的值。

2.4 有this,無return。

function A() {
    this.m = 1;
    this.n = function () {
        console.log(123);
    };
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);

2.4
普通呼叫返回undefined,而new呼叫返回一個物件,建構函式A中的this指向了該物件,所以返回物件的屬性和方法由建構函式中的this語句初始化。
ps: 需要注意的是,普通呼叫的時候,this指向了undefined,非嚴格模式下指向了widow。

2.5 有this,有return。但是return後面無返回值,或者返回基本型別值。

// 無返回值
function A() {
    this.m = 1;
    return;
}
//返回undefined型別值
function B() {
    this.m = 1;
    return undefined;
}
// 返回Number型別值
function C() {
    this.m = 1;
    return 1;
}
// 返回String型別值
function D() {
    this.m = 1;
    return '1';
}
// 返回Boolean型別值
function E() {
    this.m = 1;
    return true;
}
// 返回Null型別值
function F() {
    this.m = 1;
    return null;
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);
var c1 = C();
var c2 = new C();
console.log('c1:', c1);
console.log('c2:', c2);
var d1 = D();
var d2 = new D();
console.log('d1:', d1);
console.log('d2:', d2);
var e1 = E();
var e2 = new E();
console.log('e1:', e1);
console.log('e2:', e2);
var f1 = F();
var f2 = new F();
console.log('f1:', f1);
console.log('f2:', f2);

2.5
可以看到,普通呼叫會返回return後面的值,而new呼叫返回一個物件,建構函式A中的this指向了該物件,所以返回物件的屬性和方法由建構函式中的this語句初始化。
ps: 需要注意的是,普通呼叫的時候,this指向了undefined,非嚴格模式下指向了widow。

2.6 有this,有return。return後面是一個物件(包括函式)。

// 返回物件
function A() {
    this.m = 1;
    return {n: 2};
}
//返回函式
function B() {
    this.f = function () {
        return 1;
    };
    return function () {
        return 2;
    }
}
var a1 = A();
var a2 = new A();
console.log('a1:', a1);
console.log('a2:', a2);
var b1 = B();
var b2 = new B();
console.log('b1:', b1);
console.log('b2:', b2);

2.6
可以看到,不管是普通呼叫還是new呼叫都是返回return後面的值。
ps: 需要注意的是,普通呼叫的時候,this指向了undefined,非嚴格模式下指向了widow。

總結:對於建構函式呼叫,有如下特點:
1. 如果沒有return,返回一個新的物件,建構函式的this指向該物件。
2. 如果有return且後面的返回值不是物件(包括函式),則return語句會被忽略。
3. 如果有return且後面返回一個物件(包括函式),則返回該物件。

3. 例項物件

第2節我們已經闡述了建構函式的定義和使用方法,現在我們來看一下例項物件的定義。

例項物件:通過建構函式的new操作建立的物件是例項物件,又常常被稱為物件例項。可以用一個建構函式,構造多個例項物件。下面的f1和f2就是例項物件。

function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false

4. 原型物件

首先,我們來看兩段《JavaScrpit高階程式設計》對原型模式和原型物件的闡述:

我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。如果按照字面意思來理解,那麼prototype就是通過呼叫建構函式而建立的那個物件例項的原型物件。使用原型物件的好處是可以讓所有物件例項共享它所包含的屬性和方法

無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向函式的原型物件。在預設情況下,所有原型物件都會自動獲得一個constructor(建構函式)屬性,這個屬性包含一個指向prototype屬性所在函式的指標。

簡而言之,任何一個函式,都擁有一個prototype屬性,指向其原型物件,該原型物件也是由該函式new呼叫創造的所有例項物件的原型物件。

5. 建構函式,原型物件和例項物件的關係

5.1 指向關係

建構函式A的prototype屬性指向F與其例項物件(a1,a2,…)的原型物件A.prototype,該原型物件的constructor屬性指向建構函式A,例項物件擁有[[Prototype]]屬性(在firefox,safari和chrome上該屬性實現為__proto__)指向原型物件A.prototype

function A() {
}
var a1 = new A();
var a2 = new A();

5.1

還記得我們在前面2.1節的空函式為建構函式的圖片嗎?現在來看是不是就很清晰了。明白了其中的指向關係,我們再來看一下,建構函式中新增this語句以及在原型物件中新增屬性以後是怎樣的情況。

5.2 例項化時的資料關係

// 程式碼段5.2
function A() {
    this.m = 1;
    this.n = [1, 2];
}

A.prototype.p = 2;
A.prototype.q = [3, 4];

var a1 = new A();
var a2 = new A();

當使用建構函式新建例項物件時,各個例項物件都會擁有由this指定的屬性。
5.2

5.3 例項物件屬性賦值和使用時的關係(可以類比LHS和RHS)

5.3.1 使用時的繼承關係

使用例項物件屬性時,如果該屬性不存在於例項物件,就會使用其原型物件該屬性。
在程式碼段5.2執行之後做如下操作:

// 程式碼段5.3.1,承接程式碼段5.2
console.log('a1.m:', a1.m);
console.log('a2.m:', a2.m);

console.log('a1.n:', a1.n);
console.log('a2.n:', a2.n);

console.log('a1.p:', a1.p);
console.log('a2.p:', a2.p);

console.log('a1.q:', a1.q);
console.log('a2.q:', a2.q);

5.3.1
如圖所示,列印a1.m會找到其例項物件屬性m,而a1.p會找到其原型物件屬性p。

5.3.2 使用查詢時的先後關係(賦值時的覆蓋關係)

使用例項物件屬性時,優先從例項物件查詢該屬性,如果該屬性不存在,就會使用其原型物件該屬性。而對例項物件屬性的賦值操作,將會直接使用例項物件屬性。

// 程式碼段5.3.2.1,承接程式碼段5.3.1
a1.p = 11;

console.log('a1.p:', a1.p);
console.log('a2.p:', a2.p);

5.3.2.1

說明,a1.p是給a1添加了屬性p並賦值11,但是此時a2是沒有該屬性的,所以對a2.p的使用會查詢到A.prototype。

要注意的是,這裡例項物件屬性之間是互相獨立的,而原型物件屬性是共享的。

// 程式碼段5.3.2.2,承接程式碼段5.3.2.1
a1.n.push(3);
a1.q.push(5);

console.log('a1.n:', a1.n);
console.log('a2.n:', a2.n);

console.log('a1.q:', a1.q);
console.log('a2.q:', a2.q);

5.3.2.2
可以看到,對原型物件屬性為物件時的操作( 堆操作)會影響到其他的例項物件對該屬性的使用。

另外,還有一點要注意,如果你對物件使用的是賦值操作,並不會影響到原型屬性。不明白的同學再看一下5.3.2.1。

6. 總結

其實,我們用程式碼解釋一下new函式構造一個例項的過程。
對於

function A(m, n) {
    this.m = m;
    this.n = n;
}

var a = new A(1, 2);
console.log(a);

6.1

中的 new A(1,2)這一步操作,其實可以分解為如下四個步驟:

// 新建一個空物件obj
let obj  ={};
// obj的__proto__屬性指向原型物件
obj.__proto__ = A.prototype;
// 將建構函式的this繫結obj,傳入建構函式的引數,並將返回結果賦值給result
let result = A.apply(obj, arguments);
// 如果result存在且result是物件或者函式,則建構函式返回result,否則將返回obj
return (result && (typeof(result) === 'object' || typeof(result) === 'function')?result:obj);
  1. 新建一個空物件obj
  2. obj的proto屬性指向原型物件
  3. 將建構函式的this繫結obj,傳入建構函式的引數,並將返回結果賦值給result
  4. 如果result存在且result是物件或者函式,則建構函式返回result,否則將返回obj

我們可以試著模擬一個函式myNewA,如下:

function A(m, n) {
    this.m = m;
    this.n = n;
}

function myNewA() {
    let obj  ={};
    obj.__proto__ = A.prototype;
    let result = A.apply(obj, arguments);
    return (result && (typeof(result) === 'object' || typeof(result) === 'fucntion')?result:obj);
}
var a = myNewA(1, 2)
console.log(a);

6.2
可以看到,結果和6.1一模一樣,當然了,真正的new建構函式的過程不會是這麼簡單,我們只是通過這個例子使大家能夠加深對建構函式,原型物件和例項物件的理解。

參考