FE.ES-理解ECMA Javascript的原型

名詞解析

字面量
對象字面量 var a = {};
數組字面量 var arr = [1,2,3];
正則表達式字面量 var reg = /[a-z]/g;javascript

屬性描述符css

  • configurable:fasle是一個單向操做,同時阻止使用 delete 操做符移除既存屬性的能力。
  • enumerable控制是否在屬性枚舉操做中出現,好比 for..in 循環。
  • writeable:false時,會阻止同名屬性在 [[Prototype]] 鏈的低層被建立(遮蔽)。不能使用 = 賦值,而必須使用 Object.defineProperty(..)
  • 若是一個 foo 在 [[Prototype]] 鏈的高層某處被找到,並且它是一個 setter,那麼這個 setter 老是被調用。沒有 foo 會被添加到(也就是遮蔽在)myObject 上,這個 foo setter 也不會被重定義。
  • 對象常量: writable:falseconfigurable:false 組合
  • 防止擴展: Object.preventExtensions(..)
  • 封印: Object.seal(..) 等同於 防止擴展+configurable:false
  • 凍結: Object.freeze(..) 等同於 封印+writable:false

例:建立一個可寫的,可枚舉的,可配置的屬性pjava

var o2 = {};
Object.defineProperties(o2, {
  'p': {value: 2,   writable: true,  enumerable: true,  configurable: true },
});
Object.getOwnPropertyDescriptor( o2, "p" );
o2 = Object.create({}, {
  p: {value: 2,  writable: true,   enumerable: true,   configurable: true },
});
o2.hasOwnProperty( "p" )

原型

prototype(顯式原型)
全部的函數默認都會獲得一個公有的,不可枚舉的屬性,稱爲 prototype,它能夠指向任意的對象。__proto__是每一個對象都有的一個屬性,而prototype是函數纔會有的屬性。(經過Function.prototype.bind方法構造出來的函數是個例外,它沒有prototype屬性。)git

[[Prototype]](隱式原型,非標準寫法__proto__)
若是默認的 [[Get]] 操做不能直接在對象上找到被請求的屬性,那麼它會沿着對象的 [[Prototype]] 鏈 繼續處理。
全部的對象(包括函數)都有一個私有屬性(b.__proto__)指向它的原型對象(Foo.prototype)。該原型對象也有一個本身的原型對象(Object.__proto__) ,層層向上直到一個對象的原型對象爲null。其中,下面3種隱式原型表述在本文中相同github

  • Person.__proto__ 非標準
  • Object.getPrototypeOf(Person) 標準
  • [[Prototype]] 概念

原型圖解

1.首先了解內置對象

基本內置對象 String ,Number ,Boolean ,Object ,Function ,Array
其餘內置對象 Date,RegExp,Error正則表達式

  • 這些對象的[[Prototype]]都指向Function.prototype(Object.getPrototypeOf(String) === Function.prototype
  • Function.prototype是惟一一個typeof Xxx.prototype==='function'的prototype。其它的構造器的prototype都是'object'.
Object.prototype.constructor   === Object
Object.prototype.__proto__     === null

Function.prototype.constructor === Function
Function.prototype.__proto__   === Object.prototype

2.其次試着定義一個函數,看看發生什麼

  • 定義一個函數(function Foo)會建立原型對象(Object Foo.prototype)。
  • constructor(構造器) 是在前面 用 new 關鍵字調用的任何函數。
function Foo(){}
Foo.__proto__             === Function.prototype

Foo.prototype.constructor === Foo//function Foo(){}
Foo.prototype.__proto__   === Object.prototype

3.接着new一下,也許應該叫委託

  • new會沿者 [[Prototype]] 鏈向上找到.constructor
  • new Foo() 獲得一個新對象(咱們叫他 a),這個新對象 a 內部地被 [[Prototype]] 連接至 Foo.prototype 對象。
var b = new Foo()//new做用:構建一個被連接到另外一個對象的對象,外加這個函數要作的其餘任何事。
b.__proto__===Foo.prototype

clipboard.png

4.instanceof 和原型鏈什麼關係

object instanceof constructor用於測試constructor.prototype是否出如今object的原型鏈中的任何位置。數組

//像這樣
object.__proto__===constructor.prototype?
object.__proto__.__proto__===constructor.prototype?
object.__proto__.__proto__.__proto__===constructor.prototype?
...
b instanceof Foo//true
b instanceof Object//true

Foo.prototype.isPrototypeOf(b)//true
Object.prototype.isPrototypeOf(b)//true

Foo.prototype = {};
b instanceof Foo//false
b instanceof Object//true
Foo.prototype.isPrototypeOf(b)//false
Object.prototype.isPrototypeOf(b)//true

‘類’與‘繼承’

ES5

你們模擬了不少繼承的方法,但本質上都是兩種的變體:app

1.原型繼承

原型繼承,本質是兩個對象間創建連接,一個對象將對屬性/函數的訪問 委託 到另外一個對象上。
1.1使用 new函數

// 假設有一個須要繼承的一個類型 Animal
function Cat() {}
Cat.prototype = new Animal
// 添加一個屬性
Cat.prototype.name = 'cat'
Bar.prototype = new Foo() 確實 建立了一個新的對象,這個新對象也的確連接到了咱們但願的Foo.prototype。可是,它是用 Foo(..) 「構造器調用」來這樣作的。若是這個函數有任何反作用(好比logging,改變狀態,註冊其餘對象,向 this添加數據屬性,等等),這些反作用就會在連接時發生(並且極可能是對錯誤的對象!),而不是像可能但願的那樣,僅最終在 Bar()的「後裔」被建立時發生。

因而,咱們剩下的選擇就是使用 Object.create(..) 來製造一個新對象,這個對象被正確地連接,並且沒有調用 Foo(..)時所產生的反作用。一個輕微的缺點是,咱們不得不建立新對象,並把舊的扔掉,而不是修改提供給咱們的默認既存對象。測試

1.2使用Object.create()

function Shape() {this.x = 0;this.y = 0;}// 父類
Shape.prototype.move = function(x, y) {this.x += x;this.y += y;};// 父類的方法

function Rectangle() {Shape.call(this);} //子類

Rectangle.prototype = Object.create(Shape.prototype);// 子類續承父類
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();
  rect instanceof Rectangle; // true
  rect instanceof Shape; // true

Object.create()作了什麼?

Object.create=function (o){
    function F() {}
    F.prototype = o;
    return new F();
}

兩種操做將Bar.prototype 連接至 Foo.prototype:

// ES6 之前
// 扔掉默認既存的 `Bar.prototype`
Bar.prototype = Object.create( Foo.prototype );

// ES6+
// 修改既存的 `Bar.prototype`
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

2.構造繼承

構造繼承,爲了符合表面上咱們用 new 調用它,並且咱們觀察到它「構建」了一個對象。
本質是由於 new 讓函數調用變成了「構造器調用」

// 假設有一個須要繼承的一個類型 Animal
function Cat(name){
  Animal.call(this)
  // 添加一個屬性
  this.name = name || 'cat'
}

如何 檢查「類」繼承/自省:

錯誤方法a instanceof Foo
instanceof 只能查詢 a 的「祖先」。

勉強正確方法:用來檢查 o1 是否關聯到(委託至)o2 的幫助函數

function isRelatedTo(o1, o2) {
    function F(){}
    F.prototype = o2;
    return o1 instanceof F;
}

正確方法Foo.prototype.isPrototypeOf( a )
isPrototypeOf(..) 回答的問題是:在 a 的整個 [[Prototype]] 鏈中,Foo.prototype 出現過嗎?

3.原型繼承+構造函數繼承

function A(name){  this.name=name; }
A.prototype.sayName=function(){ console.log(this.name); }
function B(age){ this.age=age; }
//原型繼承
B.prototype=new A("mbj");  //被B的實例共享
var foo=new B(18);
foo.age;    //18,age是自己攜帶的屬性
foo.name;   //mbj,等價於foo.__proto__.name
foo.sayName(); //mbj,等價於foo.__proto__.proto__.sayName()
foo.toString();  //"[object Object]",等價於foo.__proto__.__proto__.__proto__.toString();
//構造函數繼承

原型繼承缺點:
1.全部子類共享父類實例,若是某一個子類修改了父類,其餘的子類在繼承的時候,會形成意想不到的後果。
2.構造子類實例的時候,不能給父類傳遞參數。

//構造函數繼承,避免了原型繼承缺點
function B(age,name){  this.age=age;A.call(this,name); }
var foo=new B(18,"wmy");
foo.name;     //wmy
foo.age;      //18
foo.sayName();   //undefined

構造函數繼承缺點:
1.父類的prototype中的函數不能複用

//原型繼承+構造函數繼承
function B(age,name){  this.age=age;A.call(this,name); }
B.prototype=new A("mbj");
var foo=new B(18,"wmy");
foo.name;     //wmy
foo.age;      //18
foo.sayName();   //wmy

結合了上述兩種方式的優勢,但佔用空間更大。

ES6

在 ES2015/ES6 中引入了class關鍵字,但只是語法糖,JavaScript 仍然是基於原型的。
注意:函數聲明會提高,類聲明不會。

//類聲明
class Rectangle {
    constructor(height, width,x, y) {this.height = height,this.width = width,this.x = x,this.y = y;}
    get area() {return this.calcArea()}
    calcArea() {return this.height * this.width;}
    static distance(a, b) {return Math.hypot(a.x - b.x, a.y - b.y);}
}
const square1 = new Rectangle(10, 10,5,5);
const square2 = new Rectangle(10, 10,6,6);
console.log(square1.area);
console.log(Rectangle.distance(square1 , square2 ));

//類表達式
let Rectangle = class Rectangle {constructor() {}};

extend 實現繼承

class Rectangle extends Shape {
  move() {
    super.move();//super 關鍵字用於調用對象的父對象上的函數
  }
}

常規(非可構造)對象 實現繼承

var Animal = {};
class Cat {}
Object.setPrototypeOf(Cat.prototype, Animal);

混入(Mixin)
Object.assign(target, ...sources) 方法只會拷貝源對象(...sources)自身的而且可枚舉的屬性到目標對象(target)。

function MyClass() {
     SuperClass.call(this)
     OtherSuperClass.call(this)
}

// 繼承一個類
MyClass.prototype = Object.create(SuperClass.prototype)
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype)
//Object.assign 會把  OtherSuperClass原型上的函數拷貝到 MyClass原型上,
//使 MyClass 的全部實例均可用 OtherSuperClass 的方法

// 從新指定constructor
MyClass.prototype.constructor = MyClass
MyClass.prototype.myMethod = function() {}// do a thing

當繼承的函數被調用時,this 指向的是當前繼承的對象,而不是繼承的函數所在的原型對象。

var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};
var p = Object.create(o);
// p是一個繼承自 o 的對象

p.a = 4; // 建立 p 的自身屬性 a
console.log(p.m()); // 5

派生

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

面向委託的設計

var Widget = {
    init: function(width,height){
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    },
    insert: function($where){
        if (this.$elem) {
            this.$elem.css( {
                width: this.width + "px",
                height: this.height + "px"
            } ).appendTo( $where );
        }
    }
};

var Button = Object.create( Widget );

Button.setup = function(width,height,label){
    // delegated call
    this.init( width, height );
    this.label = label || "Default";

    this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
    // delegated call
    this.insert( $where );
    this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
    console.log( "Button '" + this.label + "' clicked!" );
};

$( document ).ready( function(){
    var $body = $( document.body );

    var btn1 = Object.create( Button );
    btn1.setup( 125, 30, "Hello" );

    var btn2 = Object.create( Button );
    btn2.setup( 150, 40, "World" );

    btn1.build( $body );
    btn2.build( $body );
} );

Object.create 和 new 區別
js中__proto__和prototype的區別和關係?
你不懂JS: this 與對象原型
行爲委託

相關文章
相關標籤/搜索