(注1:如果有問題歡迎留言探討,一起學(xué)習(xí)!轉(zhuǎn)載請注明出處,喜歡可以點(diǎn)個贊哦?。?br> (注2:更多內(nèi)容請查看我的目錄。)
1. 簡介
在前面,我們對這三個概念已經(jīng)有所涉及,但是卻并未深究。事實(shí)上,如果能熟練理解掌握這三個概念和他們之間的關(guān)系,那么在學(xué)習(xí)原型鏈和繼承的知識時,會有一種撥云見霧之感。
2. 構(gòu)造函數(shù)
構(gòu)造函數(shù)其實(shí)與普通函數(shù)本身并無區(qū)別,普通函數(shù)通過new調(diào)用時,我們就稱其為構(gòu)造函數(shù)。當(dāng)然,為了區(qū)分其與普通函數(shù),構(gòu)造函數(shù)約定首字母需要大寫。下面,我們就來看一下構(gòu)造函數(shù)和普通函數(shù)使用時的區(qū)別(簡單來講就是一個函數(shù)通過new調(diào)用和不通過new調(diào)用的區(qū)別)。
2.1 一個空函數(shù)
// 空函數(shù)
function A() {}
var a1 = A();
var a2 = new A();
console.log('a1:', a1); //undefined
console.log('a2:', a2); //{}
在chrome的控制臺console運(yùn)行結(jié)果如圖所示:

直接調(diào)用返回undefined,而使用new調(diào)用返回的卻是一個空對象。這里,我們暫且不去討論_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);

可以看到,普通調(diào)用會返回return后面的值,而new調(diào)用返回空對象{}。
2.3 無this有return,但是return后面是一個對象(包括函數(shù))。
// 返回對象
function A() {
return {m: 1};
}
//返回函數(shù)
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);

可以看出,不管是普通調(diào)用還是new調(diào)用都是返回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);

普通調(diào)用返回undefined,而new調(diào)用返回一個對象,構(gòu)造函數(shù)A中的this指向了該對象,所以返回對象的屬性和方法由構(gòu)造函數(shù)中的this語句初始化。
ps: 需要注意的是,普通調(diào)用的時候,this指向了undefined,非嚴(yán)格模式下指向了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);

可以看到,普通調(diào)用會返回return后面的值,而new調(diào)用返回一個對象,構(gòu)造函數(shù)A中的this指向了該對象,所以返回對象的屬性和方法由構(gòu)造函數(shù)中的this語句初始化。
ps: 需要注意的是,普通調(diào)用的時候,this指向了undefined,非嚴(yán)格模式下指向了widow。
2.6 有this,有return。return后面是一個對象(包括函數(shù))。
// 返回對象
function A() {
this.m = 1;
return {n: 2};
}
//返回函數(shù)
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);

可以看到,不管是普通調(diào)用還是new調(diào)用都是返回return后面的值。
ps: 需要注意的是,普通調(diào)用的時候,this指向了undefined,非嚴(yán)格模式下指向了widow。
總結(jié):對于構(gòu)造函數(shù)調(diào)用,有如下特點(diǎn):
- 如果沒有return,返回一個新的對象,構(gòu)造函數(shù)的this指向該對象。
- 如果有return且后面的返回值不是對象(包括函數(shù)),則return語句會被忽略。
- 如果有return且后面返回一個對象(包括函數(shù)),則返回該對象。
3. 實(shí)例對象
第2節(jié)我們已經(jīng)闡述了構(gòu)造函數(shù)的定義和使用方法,現(xiàn)在我們來看一下實(shí)例對象的定義。
實(shí)例對象:通過構(gòu)造函數(shù)的new操作創(chuàng)建的對象是實(shí)例對象,又常常被稱為對象實(shí)例??梢杂靡粋€構(gòu)造函數(shù),構(gòu)造多個實(shí)例對象。下面的f1和f2就是實(shí)例對象。
function Foo(){};
var f1 = new Foo;
var f2 = new Foo;
console.log(f1 === f2);//false
4. 原型對象
首先,我們來看兩段《JavaScrpit高級程序設(shè)計》對原型模式和原型對象的闡述:
我們創(chuàng)建的每個函數(shù)都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。如果按照字面意思來理解,那么prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個對象實(shí)例的原型對象。使用原型對象的好處是可以讓所有對象實(shí)例共享它所包含的屬性和方法
無論什么時候,只要創(chuàng)建了一個新函數(shù),就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個prototype屬性,這個屬性指向函數(shù)的原型對象。在默認(rèn)情況下,所有原型對象都會自動獲得一個constructor(構(gòu)造函數(shù))屬性,這個屬性包含一個指向prototype屬性所在函數(shù)的指針。
簡而言之,任何一個函數(shù),都擁有一個prototype屬性,指向其原型對象,該原型對象也是由該函數(shù)new調(diào)用創(chuàng)造的所有實(shí)例對象的原型對象。
5. 構(gòu)造函數(shù),原型對象和實(shí)例對象的關(guān)系
5.1 指向關(guān)系
構(gòu)造函數(shù)A的prototype屬性指向F與其實(shí)例對象(a1,a2,...)的原型對象A.prototype,該原型對象的constructor屬性指向構(gòu)造函數(shù)A,實(shí)例對象擁有[[Prototype]]屬性(在firefox,safari和chrome上該屬性實(shí)現(xiàn)為_proto_)指向原型對象A.prototype
function A() {
}
var a1 = new A();
var a2 = new A();

還記得我們在前面2.1節(jié)的空函數(shù)為構(gòu)造函數(shù)的圖片嗎?現(xiàn)在來看是不是就很清晰了。明白了其中的指向關(guān)系,我們再來看一下,構(gòu)造函數(shù)中添加this語句以及在原型對象中添加屬性以后是怎樣的情況。
5.2 實(shí)例化時的數(shù)據(jù)關(guān)系
// 代碼段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();
當(dāng)使用構(gòu)造函數(shù)新建實(shí)例對象時,各個實(shí)例對象都會擁有由this指定的屬性。

5.3 實(shí)例對象屬性賦值和使用時的關(guān)系(可以類比LHS和RHS)
5.3.1 使用時的繼承關(guān)系
使用實(shí)例對象屬性時,如果該屬性不存在于實(shí)例對象,就會使用其原型對象該屬性。
在代碼段5.2執(zhí)行之后做如下操作:
// 代碼段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);

如圖所示,打印a1.m會找到其實(shí)例對象屬性m,而a1.p會找到其原型對象屬性p。
5.3.2 使用查找時的先后關(guān)系(賦值時的覆蓋關(guān)系)
使用實(shí)例對象屬性時,優(yōu)先從實(shí)例對象查找該屬性,如果該屬性不存在,就會使用其原型對象該屬性。而對實(shí)例對象屬性的賦值操作,將會直接使用實(shí)例對象屬性。
// 代碼段5.3.2.1,承接代碼段5.3.1
a1.p = 11;
console.log('a1.p:', a1.p);
console.log('a2.p:', a2.p);

說明,a1.p是給a1添加了屬性p并賦值11,但是此時a2是沒有該屬性的,所以對a2.p的使用會查找到A.prototype。
要注意的是,這里實(shí)例對象屬性之間是互相獨(dú)立的,而原型對象屬性是共享的。
// 代碼段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);

可以看到,對原型對象屬性為對象時的操作( 堆操作)會影響到其他的實(shí)例對象對該屬性的使用。
另外,還有一點(diǎn)要注意,如果你對對象使用的是賦值操作,并不會影響到原型屬性。不明白的同學(xué)再看一下5.3.2.1。
6. 總結(jié)
其實(shí),我們用代碼解釋一下new函數(shù)構(gòu)造一個實(shí)例的過程。
對于
function A(m, n) {
this.m = m;
this.n = n;
}
var a = new A(1, 2);
console.log(a);

中的 new A(1,2)這一步操作,其實(shí)可以分解為如下四個步驟:
// 新建一個空對象obj
let obj ={};
// obj的__proto__屬性指向原型對象
obj.__proto__ = A.prototype;
// 將構(gòu)造函數(shù)的this綁定obj,傳入構(gòu)造函數(shù)的參數(shù),并將返回結(jié)果賦值給result
let result = A.apply(obj, arguments);
// 如果result存在且result是對象或者函數(shù),則構(gòu)造函數(shù)返回result,否則將返回obj
return (result && (typeof(result) === 'object' || typeof(result) === 'function')?result:obj);
- 新建一個空對象obj
- obj的proto屬性指向原型對象
- 將構(gòu)造函數(shù)的this綁定obj,傳入構(gòu)造函數(shù)的參數(shù),并將返回結(jié)果賦值給result
- 如果result存在且result是對象或者函數(shù),則構(gòu)造函數(shù)返回result,否則將返回obj
我們可以試著模擬一個函數(shù)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);

可以看到,結(jié)果和6.1一模一樣,當(dāng)然了,真正的new構(gòu)造函數(shù)的過程不會是這么簡單,我們只是通過這個例子使大家能夠加深對構(gòu)造函數(shù),原型對象和實(shí)例對象的理解。
參考
javascript面向?qū)ο笙盗械谝黄獦?gòu)造函數(shù)和原型對象
JS入門難點(diǎn)解析10-創(chuàng)建對象
深入理解js構(gòu)造函數(shù)
JavaScript構(gòu)造函數(shù)詳解
BOOK-《JavaScript高級程序設(shè)計(第3版)》第6章