1. 程式人生 > >__proto__與constructor的使用

__proto__與constructor的使用

__proto__與constructor的使用

在 JavaScript 原型繼承結構裡面,規範中用 [[Prototype]] 表示物件隱式的原型,在 JavaScript 中用 __proto__ 表示,並且在 Firefox 和 Chrome 瀏覽器中是可以訪問得到這個屬性的,但是 IE 下不行。所有 JavaScript 物件都有 __proto__ 屬性,但只有 Object.prototype.__proto__ 為 null,前提是沒有在 Firefox 或者 Chrome 下修改過這個屬性。這個屬性指向它的原型物件。 至於顯示的原型,在 JavaScript 裡用 prototype 屬性表示

一、prototype

在JavaScript中,prototype物件是實現面向物件的一個重要機制。

每個函式就是一個物件(Function),函式物件都有一個子物件 prototype物件,類是以函式的形式來定義的。prototype表示該函式的原型,也表示一個類的成員的集合。

每個函式都有一個prototype屬性,這個屬性是指向一個物件的引用,這個物件稱為原型物件,原型物件包含函式例項共享的方法和屬性, 也就是說將函式用作建構函式呼叫(使用new操作符呼叫)的時候,新建立的物件會從原型物件上繼承屬性和方法。

在通過new建立一個類的例項物件的時候,prototype物件的成員都成為例項化物件的成員。

1、該物件被類所引用,只有函式物件才可引用;

2、在new例項化後,其成員被例項化,例項物件方可呼叫。

同時,函式是一個物件,函式物件若直接宣告成員,不用被例項化即可呼叫。

prototype

無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,預設情況下prototype屬性會預設獲得一個constructor(建構函式)屬性, 這個屬性是一個指向prototype屬性所在函式的指標,有些繞了啊,寫程式碼、上圖! 複製程式碼 程式碼如下:

function Obj(){
                this.a=[]; //例項變數
                this.fn=function(){ //例項方法

                }
            }

            var o1=new Obj();
            o1.a.push(1);
            o1.fn={};
            console.log(o1.a); //[1]
            console.log(typeof o1.fn); //object
            var o2=new Obj();
            console.log(o2.a); //[]
            console.log(typeof o2.fn); //function

在o1中修改了a和fn,而在o2中沒有改變,由於陣列和函式都是物件,是引用型別, 這就說明o1中的屬性和方法與o2中的屬性與方法雖然同名但卻不是一個引用,而是對Obj物件定義的屬性和方法的一個複製。 這個對屬性來說沒有什麼問題,但是對於方法來說問題就很大了,因為方法都是在做完全一樣的功能,但是卻又兩份複製,如果一個函式物件有上千和例項方法, 那麼它的每個例項都要保持一份上千個方法的複製,這顯然是不科學的

function Person(){

            }

根據上圖可以看出Person物件會自動獲得prototyp屬性,而prototype也是一個物件,會自動獲得一個constructor屬性,該屬性正是指向Person物件。 當呼叫建構函式建立一個例項的時候,例項內部將包含一個內部指標(很多瀏覽器這個指標名字為__proto__)指向建構函式的prototype,這個連線存在於例項和建構函式的prototype之間, 而不是例項與建構函式之間

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

            Person.prototype.printName=function(){
                alert(this.name);
            }

            var person1=new Person('Byron');
            var person2=new Person('Frank');

 

 
 

Person的例項person1中包含了name屬性,同時自動生成一個__proto__屬性,該屬性指向Person的prototype,可以訪問到prototype內定義的printName方法,大概就是這個樣子的

prototype內屬性、方法是能夠共享

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

            Person.prototype.share=[];

            Person.prototype.printName=function(){
                alert(this.name);
            }

            var person1=new Person('Byron');
            var person2=new Person('Frank');

            person1.share.push(1);
            person2.share.push(2);
            console.log(person2.share); //[1,2]

實際上當程式碼讀取某個物件的某個屬性的時候,都會執行一遍搜尋,目標是具有給定名字的屬性,搜尋首先從物件例項開始,如果在例項中找到該屬性則返回, 如果沒有則查詢prototype,如果還是沒有找到則繼續遞迴prototype的prototype物件,直到找到為止,如果遞迴到object仍然沒有則返回錯誤。 同樣道理如果在例項中定義如prototype同名的屬性或函式,則會覆蓋prototype的屬性或函式。

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

            Person.prototype.share=[];
            var person=new Person('Byron');
            person.share=0;

            console.log(person.share); //0而不是prototype中的[]

構造簡單物件

當然prototype不是專門為解決上面問題而定義的,但是卻解決了上面問題。瞭解了這些知識就可以構建一個科學些的、複用率高的物件,如果希望例項物件的屬性或函式則定義到prototype中, 如果希望每個例項單獨擁有的屬性或方法則定義到this中,可以通過建構函式傳遞例項化引數。

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

            Person.prototype.share=[];

            Person.prototype.printName=function(){
                alert(this.name);
            }

二、constructor

在 Javascript 語言中,constructor 屬性是專門為 function 而設計的,它存在於每一個 function 的prototype 屬性中。這個 constructor 儲存了指向 function 的一個引用。 
在定義一個函式(程式碼如下所示)時,JavaScript 內部會執行如下幾個動作:

1.為該函式新增一個原形(即 prototype)屬性 
2. 為 prototype 物件額外新增一個 constructor 屬性,並且該屬性儲存指向函式F 的一個引用

這樣當我們把函式 F 作為自定義建構函式來建立物件的時候,物件例項內部會自動儲存一個指向其建構函式(這裡就是我們的自定義建構函式 F)的 prototype 物件的一個屬性proto,

所以我們在每一個物件例項中就可以訪問建構函式的 prototype 所有擁有的全部屬性和方法,就好像它們是例項自己的一樣。當然該例項也有一個 constructor屬性了(從 prototype 那裡獲得的),每一個物件例項都可以通過 constrcutor 物件訪問它的建構函式

我們可以利用這個特性來完成下面的事情: 
物件型別判斷,如

var f = new F();
if(f.constructor === F) {
// do sth with F
}

其實 constructor 的出現原本就是用來進行物件型別判斷的,但是 constructor 屬性易變,不可信賴。我們有一種更加安全可靠的判定方法:instanceof 操作符。下面程式碼 
仍然返回 true

if(f instanceof F) {
// do sth with F
}

原型鏈繼承,由於 constructor 存在於 prototype 物件上,因此我們可以結合 
constructor 沿著原型鏈找到最原始的建構函式,如下面程式碼:

function Base() {}

// Sub1 inherited from Base through prototype chain
function Sub1(){}
Sub1.prototype = new Base();
Sub1.prototype.constructor = Sub1;

Sub1.superclass = Base.prototype;

// Sub2 inherited from Sub1 through prototype chain
function Sub2(){}
Sub2.prototype = new Sub1();
Sub2.prototype.constructor = Sub2;

Sub2.superclass = Sub1.prototype;

// Test prototype chain
alert(Sub2.prototype.constructor);// function Sub2(){}
alert(Sub2.superclass.constructor);// function Sub1(){}
alert(Sub2.superclass.constructor.superclass.constructor);// function Base(){}

上面的例子只是為了說明 constructor 在原型鏈中的作用,更實際一點的意義在於:一個子類物件可以獲得其父類的所有屬性和方法,稱之為繼承。

之前提到 constructor 易變,那是因為函式的 prototype 屬性容易被更改,我們用時下很流行的編碼方式來說明問題,請看下面的示例程式碼:

function F() {}
    F.prototype = {
    _name: 'Eric',
    getName: function() {
    return this._name;
    }
};

初看這種方式並無問題,但是你會發現下面的程式碼失效了:
var f = new F();
alert(f.constructor === F); // output false

怎麼回事?F 不是例項物件 f 的構造函數了嗎?當然是!只不過建構函式 F 的原型被開發者重寫了,這種方式將原有的 prototype 物件用一個物件的字面量{}來代替。而新建的物件{}只是 Object 的一個例項,系統(或者說瀏覽器)在解析的時候並不會在{}上自動新增一個 constructor 屬性,因為這是 function 建立時的專屬操作,僅當你宣告函式的時候解析器才會做此動作。然而你會發現 constructor 並不是不存在的
既然存在,那這個 constructor 是從哪兒冒出來的呢?我們要回頭分析這個物件字面量 
{}。因為{}是建立物件的一種簡寫,所以{}相當於是 new Object()。那既然{}是 Object 
的例項,自然而然他獲得一個指向建構函式 Object()的 prototype 屬性的一個引用proto,又因為 Object.prototype 上有一個指向 Object 本身的 constructor屬性。所以可以看出這個constructor其實就是Object.prototype的constructo
一個解決辦法就是手動恢復他的 constructor,下面程式碼非常好地解決了這個問題:

function F() {}
F.prototype = {
    constructor: F, /* reset constructor */
    _name: 'Eric',
    getName: function() {
    return this._name;
    }
};
var f = new F();
alert(f.constructor === F); // output true this time ^^

建構函式上怎麼還有一個 constructor ?它又是哪兒來的?細心的會發現,像 JavaScript 內建的建構函式,如 Array, RegExp, String,Number, Object, Function 等等居然自己也有一個 constructor: 

alert(typeof Array.constructor != ‘undefined’);// output true 

經過測試發現,此物非彼物它和 prototype 上 constructor 不是同一個物件,他們是共存的:

alert(typeof Array.constructor != 'undefined');// output true
alert(typeof Array.prototype.constructor === Array); // output true

在JavaScript中,每個具有原型的物件都會自動獲得constructor屬性。除了arguments、Enumerator、Error、Global、Math、RegExp、Regular Expression等一些特殊物件之外,其他所有的JavaScript內建物件都具備constructor屬性。例如:Array、Boolean、Date、Function、Number、Object、String等。所有主流瀏覽器均支援該屬性

不過這件事情也是好理解的,因為 建構函式也是函式。是函式說明它就是 Function 建構函式的例項物件,自然他內部也有一個指向 Function.prototype 的內部引用proto啦。因此我們很容易得出結論,這個 constructor(建構函式上的constructor 不是 prototype 上的)其實就是 Function 建構函式的引用:

alert(Array.constructor === Function);// output true
alert(Function.constructor === Function); // output true

建構函式(Constructor)在物件建立或者例項化時候被呼叫的方法。通常使用該方法來初始化資料成員和所需資源。構造器Constructor在js不能被繼承,因此不能重寫Overriding,但可以被過載Overloading

物件的constructor [1]  屬性用於返回建立該物件的函式,也就是我們常說的建構函式 [1]  

在一個構造方法中可以使用super關鍵字來呼叫一個父類的構造方法。

如果沒有顯式指定構造方法,則會新增預設的 constructor 方法。

如果不指定一個建構函式(constructor)方法, 則使用一個預設的建構函式(constructor)。

class Polygon {
  // ..and an (optional) custom class constructor. If one is
  // not supplied, a default constructor is used instead:
  // constructor() { }
  constructor(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
  }

  // Simple class instance methods using short-hand method
  // declaration
  sayName() {
    ChromeSamples.log('Hi, I am a ', this.name + '.');
  }

  sayHistory() {
    ChromeSamples.log('"Polygon" is derived from the Greek polus (many) ' +
      'and gonia (angle).');
  }

  // We will look at static and subclassed methods shortly
}

// Classes are used just like ES5 constructor functions:
let p = new Polygon(300, 400);
p.sayName();
ChromeSamples.log('The width of this polygon is ' + p.width);
class Square extends Polygon {
    constructor(length) {
        // 在這裡, 它呼叫了父類的建構函式, 並將 lengths 提供給 Polygon 的"width"和"height"
        super(length, length);
        // 注意: 在派生類中, 必須先呼叫 super() 才能使用 "this"。
        // 忽略這個,將會導致一個引用錯誤。
        this.name = 'Square';
    }
    get area() {
        return this.height * this.width;
    }
    set area(value) {
        // 注意:不可使用 this.area = value
        // 否則會導致迴圈call setter方法導致爆棧
        this._area = value;
    }
}

let s = new Square(5);

s.sayName();
ChromeSamples.log('The area of this square is ' + s.area);

js物件的constructor屬性返回建立該物件的函式的引用。

JavaScript的底層內部程式碼實現

// 字串:String()
var str = "張三";
document.writeln(str.constructor); // function String() { [native code] }
document.writeln(str.constructor === String); // true
// 陣列:Array()
var arr = [1, 2, 3];
document.writeln(arr.constructor); // function Array() { [native code] }
document.writeln(arr.constructor === Array); // true
// 數字:Number()
var num = 5;
document.writeln(num.constructor); // function Number() { [native code] }
document.writeln(num.constructor === Number); // true
// 自定義物件:Person()
function Person(){
this.name = "CodePlayer";
}
var p = new Person();
document.writeln(p.constructor); // function Person(){ this.name = "CodePlayer"; }
document.writeln(p.constructor === Person); // true
// JSON物件:Object()
var o = { "name" : "張三"};
document.writeln(o.constructor); // function Object() { [native code] }
document.writeln(o.constructor === Object); // true
// 自定義函式:Function()
function foo(){
alert("CodePlayer");
}
document.writeln(foo.constructor); // function Function() { [native code] }
document.writeln(foo.constructor === Function); // true
// 函式的原型:bar()
function bar(){
alert("CodePlayer");
}
document.writeln(bar.prototype.constructor); // function bar(){ alert("CodePlayer"); }
document.writeln(bar.prototype.constructor === bar); // true

 

constructor的建立

/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function(){
  //initializing是為了解決我們之前說的繼承導致原型有多餘引數的問題。當我們直接將父類的例項賦值給子類原型時。是會呼叫一次父類的建構函式的。所以這邊會把真正的構造流程放到init函式裡面,通過initializing來表示當前是不是處於構造原型階段,為true的話就不會呼叫init。
  //fnTest用來匹配程式碼裡面有沒有使用super關鍵字。對於一些瀏覽器`function(){xyz;}`會生成個字串,並且會把裡面的程式碼弄出來,有的瀏覽器就不會。`/xyz/.test(function(){xyz;})`為true代表瀏覽器支援看到函式的內部程式碼,所以用`/\b_super\b/`來匹配。如果不行,就不管三七二十一。所有的函式都算有super關鍵字,於是就是個必定匹配的正則。
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
 
  // The base Class implementation (does nothing)
  // 超級父類
  this.Class = function(){};
 
  // Create a new Class that inherits from this class
  // 生成一個類,這個類會具有extend方法用於繼續繼承下去
  Class.extend = function(prop) {
    //保留當前類,一般是父類的原型
    //this指向父類。初次時指向Class超級父類
    var _super = this.prototype;
   
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    //開關 用來使原型賦值時不呼叫真正的構成流程
    initializing = true;
    var prototype = new this();
    initializing = false;
   
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      //這邊其實就是很簡單的將prop的屬性混入到子類的原型上。如果是函式我們就要做一些特殊處理
      prototype[name] = typeof prop[name] == "function" &&
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          //通過閉包,返回一個新的操作函式.在外面包一層,這樣我們可以做些額外的處理
          return function() {
            var tmp = this._super;
           
            // Add a new ._super() method that is the same method
            // but on the super-class
            // 呼叫一個函式時,會給this注入一個_super方法用來呼叫父類的同名方法
            this._super = _super[name];
           
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            //因為上面的賦值,是的這邊的fn裡面可以通過_super呼叫到父類同名方法
            var ret = fn.apply(this, arguments);  
            //離開時 儲存現場環境,恢復值。
            this._super = tmp;
           
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
   
    // 這邊是返回的類,其實就是我們返回的子類
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
   
    // 賦值原型鏈,完成繼承
    Class.prototype = prototype;
   
    // 改變constructor引用
    Class.prototype.constructor = Class;
 
    // 為子類也新增extend方法
    Class.extend = arguments.callee;
   
    return Class;
  };
})();