prototype

ES6

ES5

javascript 是 prototype-base 的語言

ES6

提供了 class

但它只是 prototype 的封裝

var Animal = function(){};
var dog = new Animal();

dog.__proto__.__proto__.__proto__; // null

每一個物件都有其 " 原型" 物件

原型物件又指向它自己的原型物件

一直到 null 為止

也就是原型鏈 (prototype chain) 的終點

var text = 'some text here';
text.split(' ');

// ['some', 'text', 'here']

舉例來說,為什麼字串 text

split 這個方法可以用?

String.prototype.split
// split() { [native code] }

因為它的 String 原型 prototype

split 這個屬性

所以 Array, String, Number

不只是原始值 (primitive value)

也是物件實例 (Instance)

var Animal = function(name){ 
  this.name = name
};

Animal.prototype.color = 'yellow'

當我們在存取屬性時

會先從物件身上開始找起

找不到就會從 prototype 上找

最終指向 undefined

var dog = new Animal('dog');

dog.color;

// 1. 先找 dog.color 得到 undefined
// 2. 在找 dog.prototype.color 得到 "yellow"

dog.nonexistence;
// 1. 先找 dog.color 得到 undefined
// 2. 在找 dog.prototype.nonexistence
// 3. 得到 "undefined"

__proto__ 這個東西指向該物件的原型

它為非標準的瀏覽器實做

在         中 可以透過下面這兩個訪問器 (accessors) 來存取

ES6

Object.getPrototypeOf(dog) === dog.__proto__;
// true

Object.setPrototypeOf(obj, property);
dog.__proto__ === Animal.prototype;
// true

在 prototype 的屬性是所有的 instance 共用

所以當它修改時會影像到所有的實例

var MyClass = function(){};
MyClass.prototype.foo = 10;

var myObj1= new MyClass();
myObj1.foo; // 10

MyClass.prototype.foo = 20;

myObj1.foo; // 20
function Animal(name){
    this.name = name;
}
Animal.prototype.run = function(){
    console.log(this.name + "is running!!");
}

var a = new Animal("a");
var b = new Animal("b");

Animal.prototype //Animal {} 
Animal.prototype instanceof Object // true
Animal.prototype.constructor == Animal) // true

a.__proto__ == Animal.prototype //true
b.__proto__ == Animal.prototype //true
a.__proto__.__proto__ // Object {}
a.__proto__.run == a.run //true
a.__proto__.run == a.prototype.run // true

        不像其它語言有類別的定義

但任何函式都能以屬性的方式加到物件中

所以實現繼承也就是把別的物件身上的屬性

複製到自己的 prototype 上

ES5

var Animal = function(name){
    this.name = name;
};

Animal.prototype.run = function(){
    console.log(this.name + ' is running');
}

這裡有一個動物類別

我們要另外做一個 "狗" 的類別繼承它

function Dog(name){
   // 調用父類的構造函數,通過改變this指向將屬性賦值到新的實例對象 
    Animal.call(this, name);
}

Dog.prototype = new Animal();

var dog = new Dog("dog");
dog.run(); // dog is running!!

Line:3 呼叫了父類別的 constructor

Line:6 把 prototype 指向了Animal instance

 

當我們呼叫 dog.run 時,會照

dog -> dog.__proto__ (Animal) -> Animal.prototype

來搜尋該屬性

另一種繼承的方式,透過 Object.create()

var shape = {
    width: 100,
    height: 100,
    getArea: function(){
        return this.width * this.height
    }
};

var rectangle = Object.create(shape);
// rectangle 是由 shape 繼承來的物件

rectangle.width = 50;
rectangle.getArea(); // 5000

// 呼叫 getArea 時裡面的 this 指向 rectangle

另一種繼承的方式,透過 Object.create()

rectangle.__proto__ === shape; // true

rectangle; // {width: 50}

shape;
// shape -> Object.prototype -> null

rectangle
// rectangle -> shape -> Object.prototype -> null

物件屬性的枚舉 (enumerate)

for(var i in shape) {
  console.log(i);
}

// width
// height
// getArea

透過 for ... in 來枚舉物件身上的屬性

但是

var rectangle = Object.create(shape);
rectangle.width = 50; // rectangle 只有 width 這個屬性

for(var i in rectangle) {
  console.log(i);
}

// width
// height
// getArea

for ... in 也會把原型鏈上的屬性印出來

解法 1: Object.hasOwnProperty()

var rectangle = Object.create(shape);
rectangle.width = 50; // rectangle 只有 width 這個屬性

for(var i in rectangle) {
  if(rectangle.hasOwnProperty(i)) console.log(i)
}

// width

解法 2: Object.keys()

var keys = Object.keys(rectangle);

keys.forEach(function(key, idx){
    console.log(key);
});

// width

解法 3: Object.definedProperties()

var shape = {};
Object.defineProperties(shape, {
  'width': {
    value: 100,
    writable: true,
    enumerable: false,
  },
  'height': {
    value: 100,
    writable: true,
    enumerable: false,
  }
  // etc.
});

解法 3: Object.definedProperties()

var rectangle = Object.create(shape);

for(i in rectangle) {
  console.log(i);
}

// nothing print out

不要擴充原生物件的原型

Array.prototype.sum = function(){
    var sum = this.reduce(
        (sum, current) => sum + current
    , 0);

    return sum;
};

var ary = [1, 2, 3];
ary.sum(); // 6

關於繼承,還有一些事

function Dog(name){
    Animal.call(this, name);
}

Dog.prototype = new Animal();

var dog = new Dog("dog");
dog.__proto__;
// Animal {name: undefined}
// dog 的 prototype 多了不必要的屬性 "name"
function Dog(name){
    Animal.call(this, name);
}

Dog.prototype = new Animal();

var dog = new Dog("dog");
dog.constructor === Animal; // true
dog.constructor === Dog; // false
var F = function(){};
F.prototype = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;
function objCreate(prototype){
  var F = function(){};
  F.prototype = prototype;
  return new F();
}
function inherit(subclass, parentclass){
  subclass.prototype = objCreate(parentclass.prototype);
  subclass.prototype.constructor = subclass;
}

改良

封裝

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

Animal.prototype.run = function(){
    console.log(this.name + "is running!!");
}

function Dog(name){
    Animal.call(this,name);
}

inherit(Dog, Animal);
var dog = new Dog('dog');
dog.run();//dog is running!!

回頭修改一下前面的程式

以上是最早期的繼承方式

Class

ES6

class declaration

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

const p = new Polygon(100,100);
// Polygon {height: 100, width: 100}

No Hoisting

var p = new Polygon(); // ReferenceError

class polygon {}

Class expressions

// unnamed
var Polygon = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

console.log(Polygon.name);
// "Polygon"

Class expressions

// named
var Polygon = class Polygon2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

console.log(Polygon.name);
// "Polygon2"

Extend

class Rectangle extends Polygon {
    // do something here...
}

Prototype methods

class Rectangle extends Polygon {
    // Getter
    get area() {
        return this.calcArea();
    }

    // Method
    calcArea(){
        return this.height * this.width;
    }
}

const square = new Rectangle(100, 100);

console.log(square.area);
// 10000

Override

class Polygon{
  constructor(w, h){
    this.w = w;
    this.h = h;
  }
  
  getShape(){
    return "polygon"
  }
}

Override

class Rectangle extends Polygon{
  getShape(){
    return "Rectangle"
  }
}

const square = new Rectangle(100, 100);
square.getShape(); // "Rectangle"

Super

class Rectangle extends Polygon{
  getShape(){
    const rs = super.getShape();
    return `quadrilateral of ${rs}`;
  }
}

const square = new Rectangle(100, 100);
square.getShape(); 
// "quadrilateral of polygon"

"Super" in constructor

class Triangle extends Polygon{
  constructor(sideA, sideB, sideC){
    this.sideA = sideA;
    this.sideB = sideB;
    this.sideC = sideC;
  }
}

var t = new Triangle();

// ReferenceError: Must call super constructor in derived class

"Super" in constructor

  • sub Class 的 constructor 一定要呼叫 super
  • 如果 sub Class 沒有 constructor 會自己建立一個並呼叫 super
  • ES5 的繼承是先創造子類,再把父類的東西添加上去
  • ES6 的繼承是先創造父類,而後在被子類修改

"Super" in constructor

class Triangle extends Polygon{
  constructor(sideA, sideB, sideC){
    super(0 ,0);
    this.sideA = sideA;
    this.sideB = sideB;
    this.sideC = sideC;
  }
}

var t = new Triangle(3, 4, 5);

// Triangle {w: 0, h: 0, sideA: 3, sideB: 4, sideC: 5}

Static mehods

class Point {
  constructor(x, y){
    this.x = x;
    this.y = y;
  }

  static distance(p1, p2){
    const dx = p1.x - p2.x;
    const dy = p1.y - p2.y;
    return Math.hypot(dx, dy);
  }
}
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);

Point.distance(p1, p2); // 5

Instance properties

class Polygon{
  constructor(w, h){
    this.w = w;
    this.h = h;
  }

  static w = 10;
}
const rect = new Polygon(100, 100);

rect.w; // 100;
Polygon.w; // 10

Prototype 上的 primitive

class MyClass{};

MyClass.prototype.foo = 100;

var c1 = new MyClass();
c1.foo; // 100

var c2 = new MyClass();
c2.__proto__.foo = 200;

c1.foo; // 200

當你想要在 prototype 上建立共用的 primitive 屬性,目前只能這樣做

這份草案通過就可以改成這樣

class MyClass{
  foo = 100;
};


var c1 = new MyClass();
c1.foo; // 100

END

Quiz

請修改下面的物件,當它被設定屬性 width 時

如果數值不是整數,會四捨五入

如果輸入的數值非法 (包含負數),會變成 -1

var shape = {
  width: 0,
};

shape.width = 3.4;
console.log(shape.width); // 3

shape.width = -infinity;
console.log(shape.width); // -1

// hint
// Object.defineProperty()

承上題,請改用 class 方式達成

class Shape {
  // ...
}
var shape = new Shape();

shape.width = 3.4;
console.log(shape.width); // 3

// hint: get set

javascript prototype

By mangogan

javascript prototype

  • 539