JS 面向?qū)ο缶幊讨畼?gòu)造函數(shù)和原型

構(gòu)造函數(shù)

構(gòu)造函數(shù)其實也是一個對象

var Fun  = function () {
  this.name = '測試'
}

上面的Fun就是一個構(gòu)造函數(shù),為了與普通函數(shù)區(qū)別,構(gòu)造函數(shù)通常第一個首字母大寫。構(gòu)造函數(shù)有兩個特點:

  1. 函數(shù)體內(nèi)部使用了this關(guān)鍵字,代表了所要生成的對象實例。
  2. 生成對象的時候,必須使用new命令。
  • new 關(guān)鍵字
    new關(guān)鍵字的作用就是執(zhí)行構(gòu)造函數(shù),返回一個實例對象。
var Person= function () {
  this.name = '測試'
}
var Tom = new Person()
Tom.name // 測試

上面代碼通過new命令,讓構(gòu)造函數(shù)Person生成一個實例對象,保存在變量Tom中。這個新生成的實例對象,從構(gòu)造函數(shù)Person得到了name屬性。new命令執(zhí)行時,構(gòu)造函數(shù)內(nèi)部的this,就代表了新生成的實例對象,this.name表示實例對象有一個name屬性,值是測試。

  1. 使用new命令時,根據(jù)需要,構(gòu)造函數(shù)也可以接受參數(shù)。
  2. new命令本身就可以執(zhí)行構(gòu)造函數(shù),所以后面的構(gòu)造函數(shù)可以帶括號,也可以不帶括號。但是為了表示這里是函數(shù)調(diào)用,推薦使用括號。

如果忘了使用new命令,直接調(diào)用構(gòu)造函數(shù)會發(fā)生什么事?

這種情況下,構(gòu)造函數(shù)就變成了普通函數(shù),并不會生成實例對象。而且由于后面會說到的原因,this這時代表全局對象,將造成一些意想不到的結(jié)果。

var Person= function (name) {
  this.name = '測試'
}
var Tom = new Person('Tom')
Tom.name // Tom

var p = Person()
p // undefind
name // 測試

new 命令的原理
使用new命令時,它后面的函數(shù)依次執(zhí)行下面的步驟。

  1. 創(chuàng)建一個空對象,作為將要返回的對象實例。
  2. 將這個空對象的原型,指向構(gòu)造函數(shù)的prototype屬性。
  3. 將這個空對象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
  4. 開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼。
function _new(/* 構(gòu)造函數(shù) */ constructor, /* 構(gòu)造函數(shù)參數(shù) */ params) {
  // 將 arguments 對象轉(zhuǎn)為數(shù)組
  var args = [].slice.call(arguments);
  // 取出構(gòu)造函數(shù)
  var constructor = args.shift();
  // 創(chuàng)建一個空對象,繼承構(gòu)造函數(shù)的 prototype 屬性
  var context = Object.create(constructor.prototype);
  // 執(zhí)行構(gòu)造函數(shù)
  var result = constructor.apply(context, args);
  // 如果返回結(jié)果是對象,就直接返回,否則返回 context 對象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 實例
var actor = _new(Person, '張三', 28);

如果構(gòu)造函數(shù)里面有return,并且return一個非對象,new 命令執(zhí)行該函數(shù)時會忽略該return,返回“構(gòu)造”后的 this對象;如果return的是一個與this無關(guān)的對象,new會將該對象返回,而不是this對象。
另一方面,如果對普通函數(shù)(內(nèi)部沒有this關(guān)鍵字的函數(shù))使用new命令,則會返回一個空對象。

/** rerurn 非對象 */
var Car= function () {
  this.price = 1000;
  return 10;
};
(new Car()).price === 1000 // true
(new Car()) === 10 // false

/** return 一個與this無關(guān)的對象 */
var Animal = function () {
  this.type = 'cat'
  return {name: '小花'}
}
(new Animal()) // {name: '小花'}
(new Animal()).type // undefind
(new Animal()).name // 小花

/** new 一個普通函數(shù) */
var Dog = function () {
  return 'this is a Dog。'
}
var d = new Dog()
d // {}
typeof d // "object"
  • Object.create() 創(chuàng)建實例對象
    構(gòu)造函數(shù)作為模板,可以生成實例對象。但是,有時拿不到構(gòu)造函數(shù),只能拿到一個現(xiàn)有的對象。我們希望以這個現(xiàn)有的對象作為模板,生成新的實例對象,這時就可以使用Object.create()方法。
var person1 = {
  name: '張三',
  age: 38,
  greeting: function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};

var person2 = Object.create(person1);

person2.name // 張三
person2.greeting() // Hi! I'm 張三.

上面代碼中,對象person1是person2的模板,后者繼承了前者的屬性和方法。

  • 構(gòu)造函數(shù)的缺點
    通過構(gòu)造函數(shù)為實例對象定義屬性,雖然很方便,但是有一個缺點。每一次new都是將構(gòu)造函數(shù)復(fù)制一份賦值給新的實例對象,同一個構(gòu)造函數(shù)的多個實例之間,無法共享屬性,從而造成對系統(tǒng)資源的浪費。
var Animal = function (name) {
  this.name = name
  this.type = 'cat'
  this.eat = function () {
    console.log('animal is eat' )  
  }
}
var animal1 = new Animal('小花')
var animal2 = new Animal('小黃')
animal1.eat() === animal2.eat() // false

上面代碼中,animal1animal2是同一個構(gòu)造函數(shù)的兩個實例,它們都具有eat方法。由于eat方法是生成在每個實例對象上面,所以兩個實例就生成了兩次。也就是說,每新建一個實例,就會新建一個eat方法。這既沒有必要,又浪費系統(tǒng)資源,因為所有eat方法都是同樣的行為,完全應(yīng)該共享。

這個問題的解決方法,就是 JavaScript 的原型對象(prototype)。

原型

  • prototype屬性的作用
    JavaScript 規(guī)定,每個函數(shù)都有一個prototype屬性,指向一個對象。
    對于普通函數(shù)來說,該屬性基本無用。但是,對于構(gòu)造函數(shù)來說,生成實例的時候,該屬性會自動成為實例對象的原型。
function fn() {}
typeof fn.prototype // "object"


function Animal(name) {
  this.name = name
}
Animal.prototype.color = 'white'

var cat1 = new Animal('小花')
var cat2 = new Animal('小黃')

cat1.color // 'white'
cat2.color // 'white'

Animal.prototype.color = 'yellow'
cat1.color // 'yellow'
cat2.color // 'yellow'

這里的colorcat1cat2都共享的一個屬性,當(dāng)Animal的color屬性改變時,cat1和cat2對應(yīng)的屬性也會改變。
當(dāng)實例對象本身沒有某個屬性或方法的時候,它會到原型對象去尋找該屬性或方法。這就是原型對象的特殊之處。

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

如果實例本身有這個屬性或方法,就不會去原型對象上尋找。

  • 原型鏈
    JavaScript 規(guī)定,所有對象都有自己的原型對象(prototype)。一方面,任何一個對象,都可以充當(dāng)其他對象的原型;另一方面,由于原型對象也是對象,所以它也有自己的原型。因此,就會形成一個“原型鏈”(prototype chain):對象到原型,再到原型的原型……
    所有對象的原型最終都可以上溯到Object.prototype,即Object構(gòu)造函數(shù)的prototype屬性。也就是說,所有對象都繼承了Object.prototype的屬性。這就是所有對象都有valueOftoString方法的原因,因為這是從Object.prototype繼承的。
    Object.prototype的原型是null。null沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null。
Object.getPrototypeOf(Object.prototype) // null

讀取對象的某個屬性時,JavaScript 引擎先尋找對象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的Object.prototype還是找不到,則返回undefined。如果對象自身和它的原型,都定義了一個同名屬性,那么優(yōu)先讀取對象自身的屬性,這叫做“覆蓋”(overriding)。

注意,一級級向上,在整個原型鏈上尋找某個屬性,對性能是有影響的。所尋找的屬性在越上層的原型對象,對性能的影響越大。如果尋找某個不存在的屬性,將會遍歷整個原型鏈。

  • constructor 屬性
    prototype對象有一個constructor屬性,默認(rèn)指向prototype對象所在的構(gòu)造函數(shù)。
function F () {}
F.prototype.constructor === F // true

由于constructor屬性定義在prototype對象上面,意味著可以被所有實例對象繼承。

function F () {}
F.prototype.constructor === F // true
var fn  = new F()
fn.constructor === F // true
fn.constructor === F.prototype.constructo // true
fn.hasOwnPrototype('constructor') // false

上面的例子中,fnF的實例對象,fn沒有自己的constructor屬性,該屬性其實是讀取原型鏈上面的F.prototype.constructor屬性。

constructor屬性的作用是,可以得知某個實例對象,到底是哪一個構(gòu)造函數(shù)產(chǎn)生的。
另一方面,有了constructor屬性,就可以從一個實例對象新建另一個實例。

function Constr() {}
var x = new Constr()

var y = new x.constructor()
y instanceof Constr // true

constructor屬性表示原型對象與構(gòu)造函數(shù)之間的關(guān)聯(lián)關(guān)系,如果修改了原型對象,一般會同時修改constructor屬性,防止引用的時候出錯。

function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person // true

Person.prototype = {
  method: function () {}
};

Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true

上面代碼中,構(gòu)造函數(shù)Person的原型對象改掉了,但是沒有修改constructor屬性,導(dǎo)致這個屬性不再指向Person。由于Person的新原型是一個普通對象,而普通對象的contructor屬性指向Object構(gòu)造函數(shù),導(dǎo)致Person.prototype.constructor變成了Object。

所以,修改原型對象時,一般要同時修改constructor屬性的指向。

// 壞的寫法
C.prototype = {
  method1: function (...) { ... },
  // ...
}

// 好的寫法
C.prototype = {
  constructor: C,
  method1: function (...) { ... },
  // ...
}

// 更好的寫法
C.prototype.method1 = function (...) { ... }

如果不能確定constructor屬性是什么函數(shù),還有一個辦法:通過name屬性,從實例得到構(gòu)造函數(shù)的名稱。

function Foo() {}
var f = new Foo()
f.constructor.name // "Foo"
  • instanceof 運算符
    instanceof運算符返回一個布爾值,表示對象是否為某個構(gòu)造函數(shù)的實例(檢查的是整個原型鏈)。
    instanceof運算符的左邊是實例對象,右邊是構(gòu)造函數(shù)。它會檢查右邊構(gòu)建函數(shù)的原型對象(prototype),是否在左邊對象的原型鏈上。因此,下面兩種寫法是等價的。
v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)

// instanceof檢查的是整個原型鏈
var d = new Date()
d instanceof Date // true
d instanceof Object // true

上面代碼中,d同時是DateObject的實例,因此對這兩個構(gòu)造函數(shù)都返回true。

instanceof的原理是檢查右邊構(gòu)造函數(shù)的prototype屬性,是否在左邊對象的原型鏈上。有一種特殊情況,就是左邊對象的原型鏈上,只有null對象。這時,instanceof判斷會失真。

var obj = Object.create(null)
typeof obj // "object"
obj instanceof Object // false
Object.create(null) instanceof Object // false

注意,instanceof運算符只能用于對象,不適用原始類型的值。

var s = 'hello'
s instanceof String // false

此外,對于undefinednull,instanceof運算符總是返回false

undefined instanceof Object // false
null instanceof Object // false

利用instanceof運算符,還可以巧妙地解決,調(diào)用構(gòu)造函數(shù)時,忘了加new命令的問題。

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo
    this._bar = bar
  } else {
    return new Fubar(foo, bar)
  }
}

參考文獻:
阮一峰JavaScript 標(biāo)準(zhǔn)參考教程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容