js基礎知識溫習:構造函數與原型

構造函數

構造函數主要用於初始化新對象。按照慣例,構造函數名第一個字母都要大寫。數組

構造函數有別於其它函數在於它使用new操做符來調用生成一個實例對象。換句話說,若是一個函數使用new操做符來調用,則將其稱爲構造函數。安全

function User(name, age) {
  this.name = name;
  this.age = age;
}

// 調用
var jenemy = new User('jenemy', 25);
jenemy.name; // jenemy

與函數調用和方法調用的不一樣點在於,構造函數調用是將一個全新的對象做爲this變量的值,並隱式返回這個新對象做爲調用的結果。函數

typeof User; // function
typeof jenemy; // object

在JavaScript中每個對象都有一個constructor屬性指向建立這個對象的函數,函數一樣也是一個對象。所以有this

Person.constructor === Function; // true
jenemy.constructor === User; // true

若是調用構造函數時忘記使用new操做符,那麼構造函數將做爲一個普通函數調用,此時this將會被綁定到全局對象中。firefox

var xiaolu = User('xiaolu', 25);

xiaolu.name; // Uncaught TypeError: Cannot read property 'name' of undefined
window.name; // xiaolu
window.age; // 25

更加健壯的方式是不管如何調用都按構造函數來工做。prototype

function User(name, age) {
  if (!(this instanceof User)) {
    return new User(name, age);
  }

  this.name = name;
  this.age = age;
}

var jenemy = new User('jenemy', 25);
var xiaolu = User('xiaolu', 25);

jenemy instanceof User; // true
xiaolu instanceof User; // true

這種模式雖然可以解決問題,但帶來了另一個問題:執行了二次User()函數的調用,所以代價有點高。此外,若是參數是可變的,這種方式也很難適用。設計

這裏可使用ES5的Object.create()來解決上述問題。指針

function User(name, age) {
  var self = this instanceof User ? this : Object.create(User.prototype);
  self.name = name;
  self.age = age;

  return self;
}

Object.create()方法是建立一個擁有指定原型和若干個指定屬性的對象。這裏須要注意的是它的第二個參數和Object.defineProperties()的第二個參數是同樣的。code

因爲Object.create()只有在ES5纔是有效的,所以須要對Object.create()進行Polyfill對象

if (typeof Object.create != 'function') {
  // 使用匿名函數封裝全部私有變量
  Object.create = (function() {
    function Temp() {};

    // 更加安全的引用Object.prototype.hasOwnProperty
    var hasOwn = Object.prototype.hasOwnProperty;

    return function(O) {
      // 若是 O 不是 Object 或者 null,拋出一個 TypeError 異常
      if (typeof O != 'object') {
        throw TypeError('Object prototype may only be an Object or null');
      }

      Temp.prototype = O;
      var obj = new Temp();
      Temp.prototype = null; // 釋放臨時對象資源

      // 若是存在參數 Properties,而不是undefined,那麼就把自身屬性添加到 obj 上
      if (arguments.length > 1) {
        var Properties = Object(arguments[1]);
        for (var prop in Properties) {
          if (hasOwn.call(Properties, prop)) {
            obj[prop] = Properties[prop];
          }
        }
      }

      return obj;
    };
  }) ();
}

上述polyfill實現了Object.create()的全部功能,其實這裏只須要每個參數就能夠了,所以能夠簡化一下

if (Object.create != 'function') {
  Object.create = function(O) {
    function Temp() {};

    if (typeof O != 'object') {
      throw TypeError('Object prototype may only be an Object or null');
    }

    Temp.prototype = O;
    return new Temp();
  };
}

原型(prototype)

在JavaScript中prototype屬性保存了引用類型全部實例方法的真正所在。拿數組操做來說,push()方法其實是保存在prototype名下,只不過是經過其對象的實例訪問罷了。

// 實例化一個數組對象
var person = new Array();
// 調用實例化方法push
person.push('jenemy');

// 一樣也能夠直接調用Array.prototype.push方法。
// 同時注意將this指向當前person數組對象
Array.prototype.push.call(person, 'xiaolu');

person.length; // 2

不管何時,只要咱們建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。

在上面介紹構造函數的時候提到過每一個對象都有一個constructor屬性指向建立它的函數。所以全部原型對象都會默認得到一個constructor屬性指向prototype屬性所在函數的指針。而後,當調用構造函數實例化一個新對象後,該實例內部會有一個標準的指針 [[Prototype]] 指向構造函數的實例對象。雖然沒有一個標準的方式去訪問 [[Prototype]],但 firefox、Safari 和 Chrome在每一個對象上都支持一個屬性__proto__

注意一點的是__proto__實際上只存在於構造函數實例與構造函數原型之間,而不是存在於實例與構造函數之間

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

Person.prototype.getName = function() {
  return this.name;
}

var jenemy = new Person('jenemy', 25);
var xiaolu = new Person('xiaolu', 25);

jenemy.name; // jenemy
xiaolu.getName(); // xiaolu

爲了便於理解各對象之間的關係,咱們將其圖形化展現:

|------------------------1---------------------------|
        v                                                    |
|-----------------|                  |---------------------| |
|  Person         |            ----->|  Person.prototype   | |
|-----------------|            |     |---------------------| |
|  prototype    |·|-------------     |  constructor      |·|--
|-----------------|            |     |---------------------|
|  name | String  |            |     |  getName | Function |
|-----------------|            |     |---------------------|
|  age  | String  |            2
|-----------------|            |
                               |---------------------------|
                               |                           |
|-----------------|            |     |-------------------| |
|  jenemy         |            |     |  xiaolu           | |
|-----------------|            |     |-------------------| |
| [[Prototype]] |·|------------|     | [[prototype]]   |·|-|
|-----------------|                  |-------------------|
| name | 'jenemy' |                  | name | 'xiaolu'   |
|-----------------|                  |-------------------|
| age  | 25       |                  | age  | 25         |
|-----------------|                  |-------------------|

而後咱們再用代碼來驗證一下圖形所展現的對象之間的關係

// 驗證線路1
Person.prototype.constructor === Person; // true

// 驗證線路2
Person.prototype.isPrototypeOf(jenemy); // true
Object.getPrototypeOf(xiaolu) === Person.prototype; // true
jenemy.__proto__ === Person.prototype; // true

// 因爲jenemy是由new Person()後獲得的實例化對象,所以有
jenemy.constructor === Person; // true

這裏的Object.isPrototypeOf()方法用於檢查傳入的對象是不是傳入對象的原型。而Object.getPrototypeOf()方法返回指定對象的原型(也就是該對象的內部屬性[[Prototype]]的值)。

上面咱們有使用__proto__來獲取對象的原型,但並非全部的JavaScript環境都支持經過它來獲取對象的原型,所以官方給出了一個標準解決方案就是使用Object.getPrototypeOf()方法。另外須要注意的是,擁有null原型的對象沒有這個特殊的__proto__屬性。

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

var jenemy = new Person('jenemy', 25);
var empty = Object.create(null);

'__proto__' in jenemy; // true
'__proto__' in empty; // false
Object.getPrototypeOf(empty); // null

因爲Object.getPrototypeOf()方法是ES5中提供的方法,對於那些沒有提供ES5 API的環境,也能夠利用__proto__屬性來實現Object.getPrototypeOf()函數。

if (typeof Object.getPrototypeOf === 'undefined') {
  Object.getPrototypeOf = function(obj) {
    var t = typoef obj;
    if (!obj || (t!== 'object' && t!== 'function')) {
      throw new TypeError('not an object');
    }
    return obj.__proto__;
  }
}

在建立原型屬性時,咱們一樣可使用對象字面量語法:

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

Person.prototype = {
  getName: function() {
    return this.name;
  }
}

Person.prototype.constructor === Person; // false
Person.proottype.constructor; // Object

而後,咱們發現這裏Person.prototype.constructor並無指向建立它的構造函數,而是指向了Object,緣由在於咱們重寫了Person.prototype對象,致使Person.prototype指向了Object,面Object.prototype.constructor原本就指向'Object'。解決辦法是手機將Person.prototype.constructor指向Person

Person.prototype = {
  constructor: Person,
  getName: function() {
    return this.name;
  }
}

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

獲取對象的屬性列表

對於一個對象的屬性遍歷,最早想到的就是使用in操做符在for-in循環中使用。經過in操做符能夠訪問實例中和原型中的可枚舉的屬性。

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

  Person.prototype.prop1 = 1;

  var jenemy = new Person('jenemy', 25);
  jenemy.addr = 'shanghai';

  'name' in jenemy; // true
  'prop1' in jenemy; // true
  'addr' in jenemy; // true

另外一個就是使用ES5提供的Object.keys()方法,它會返回一個由給定對象的全部可枚舉自身屬性的屬性名組成的數組,數組中屬性名的排列順序和使用for-in循環遍歷該對象時返回的順序一致(二者的主要區別是for-in會枚舉原型鏈中的屬性)。

Object.keys(jenemy); // ["name", "age", "addr"]

最後一個是使用ES5提供的Object.getOwnPropertyNames()方法,它會返回對象全部的實例屬性,包括可枚舉的和不可枚舉的屬性。

Object.getOwnPropertyNames(jenemy); // ["0", "1", "2", "length"]

這裏length屬性是Object對象的一個不可枚舉的屬性,所以會被輸出。

區分實例屬性和原型屬性

在JavaScript中咱們能夠任意修改對象的屬性和方法,甚至能夠修改內置原型方法。固然,咱們不是不建議修改內置對象方法和屬性,這樣會致使依賴該方法或者屬性的其它調用者發生沒法預期的結果。

當爲對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。也就是說,它會阻止咱們訪問原型中的那個屬性,但不會修改那個屬性。固然,使用delete操做符能夠徹底刪除實例屬性,從而從新訪問原型中的屬性。

使用hasOwnProperty()方法能夠檢測一個屬性是存在於實例中,仍是存在於原型中。只有屬性存在於實例中時纔會返回true。

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

Person.prototype.getName = function() {
  return this.name;
}

var jenemy = new Person('jenemy', 25);
jenemy.getName(); // jenemy
jenemy.hasOwnProperty('getName'); // false
// 自定義實例屬性
jenemy.getName = function() {
  return '個人名字叫' + this.name + '我會屏蔽掉原型中的屬性值。';
}

jenemy.getName(); // 個人名字叫jenemy我會屏蔽掉原型中的屬性值。
jenemy.hasOwnProperty('getName'); // true

delete jenemy.getName;
jenemy.getName(); // jenemy
jenemy.hasOwnProperty('getName'); // false

參考

-《JavaScript高級程序設計》(第3版)
-《JavaScript面向對象精要》
-《Effective JavaScript》

相關文章
相關標籤/搜索