溫故知新之javascript面向對象

基本概念

類和實例是大多數面向對象編程語言的基本概念javascript

  • 類:類是對象的類型模板html

  • 實例:實例是根據類建立的對象
    可是,JavaScript語言的對象體系,不是基於「類」的,而是基於構造函數(constructor)和原型鏈(prototype)。了與普通函數區別,構造函數名字的第一個字母一般大寫。java

構造函數的特色有兩個。編程

  • 函數體內部使用了this關鍵字,表明了所要生成的對象實例。segmentfault

  • 生成對象的時候,必需用new命令數組

new與構造函數

  1. new命令自己就能夠執行構造函數,因此後面的構造函數能夠帶括號,也能夠不帶括號。app

    下面兩行代碼是等價的。編程語言

    var v = new Vehicle();
    var v = new Vehicle;

    應該很是當心,避免出現不使用new命令、直接調用構造函數的狀況。爲了保證構造函數必須與new命令一塊兒使用,一個解決辦法是,在構造函數內部使用嚴格模式,即第一行加上use strict函數

    原理:因爲在嚴格模式中,函數內部的this不能指向全局對象,默認等於undefined,致使不加new調用會報錯(JavaScript不容許對undefined添加屬性)。oop

  2. new命令的原理

    • 建立一個空對象,做爲將要返回的對象實例

    • 將這個空對象的原型,指向構造函數的prototype屬性

    • 將這個空對象賦值給函數內部的this關鍵字

    • 開始執行構造函數內部的代碼

    即:

    var obj  = {};
    obj.__proto__ = Base.prototype;
    Base.call(obj);
  3. 構造函數的return
    若是構造函數內部有return語句,並且return後面跟着一個對象,new命令會返回return語句指定的對象;不然,就會無論return語句,返回this對象。

  4. 若是對普通函數(內部沒有this關鍵字的函數)使用new命令,則會返回一個空對象
    這裏遇到了一個問題,問題描述以下普通函數用new測試的時候箭頭函數報錯了

建立對象

JavaScript對每一個建立的對象都會設置一個原型,指向它的原型對象。
當咱們用obj.xxx訪問一個對象的屬性時,JavaScript引擎先在當前對象上查找該屬性,若是沒有找到,就到其原型對象上找,若是尚未找到,就一直上溯到Object.prototype對象,最後,若是尚未找到,就只能返回undefined
例如,建立一個Array對象:
var arr = [1, 2, 3];
其原型鏈是:
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype定義了indexOf()、shift()等方法,所以你能夠在全部的Array對象上直接調用這些方法。
很容易想到,若是原型鏈很長,那麼訪問一個對象的屬性就會由於花更多的時間查找而變得更慢,所以要注意不要把原型鏈搞得太長。

new Student()

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}
var xiaoming= new Student('xiaoming'),
    xiaohong= new Student('xiaohong');

xiaoming ↘
xiaohong -→ Student.prototype ----> Object.prototype ----> null
xiaojun ↗
用new Student()建立的對象還從原型上得到了一個constructor屬性,它指向函數Student自己:

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

constructor

constructor屬性的做用,是分辨原型對象到底屬於哪一個構造函數。
function F() {};
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false
上面代碼表示,使用constructor屬性,肯定實例對象f的構造函數是F,而不是RegExp。

構造函數繼承 VS 原型鏈繼承

xiaoming.name
//"xiaoming"
xiaohong.name
//"xiaohong"
xiaoming.hello
/*function() {
        alert('Hello, ' + this.name + '!');
    }
*/
xiaohong.hello
/*function() {
        alert('Hello, ' + this.name + '!');
    }
*/
xiaoming.hello === xiaohong.hello
//false

xiaoming和xiaohong各自的name不一樣,這是對的,不然咱們沒法區分誰是誰了。

xiaoming和xiaohong各自的hello是一個函數,但它們是兩個不一樣的函數,雖然函數名稱和代碼都是相同的!

若是咱們經過new Student()建立了不少對象,這些對象的hello函數實際上只須要共享同一個函數就能夠了,這樣能夠節省不少內存。

要讓建立的對象共享一個hello函數,根據對象的屬性查找原則,咱們只要把hello函數移動到xiaoming、xiaohong這些對象共同的原型上就能夠了,也就是Student.prototype

修改代碼以下:

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

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};
xiaoming.hello === xiaohong.hello
//true

繼承方式對比

  1. 借用構造函數繼承
    基本思想很簡單,在子類型的構造函數內部調用父類型的構造函數:

    function SuperType(){
     this.colors = ["red", "blue", "green"];
    }
    function SubType(){
         //繼承了 SuperType
         SuperType.call(this);
    }
    var instance1 = new SubType();
    instance1.colors.push("black");
    alert(instance1.colors); //"red,blue,green,black"
    var instance2 = new SubType();
    alert(instance2.colors); //"red,blue,green"

    問題:方法都在構造函數內部定義,函數的複用性就無從談起了。在超類型的原型中定義的方法,對子類型而言是不可見的。考慮這些問題,借用構造函數也是不多單獨使用。

  2. 組合繼承
    實現的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過constructor stealing技術實現對實例屬性的繼承。

    function SuperType(name){
         this.name = name;
         this.colors = ["red", "blue", "green"];
    }
    SuperType.prototype.sayName = function(){
         alert(this.name);
    };
    function SubType(name, age){
         //繼承屬性
         SuperType.call(this, name);
         this.age = age;
    }
    
    //繼承方法
    SubType.prototype = new SuperType();
    SubType.prototype.constructor = SubType;
    SubType.prototype.sayAge = function(){
         alert(this.age);
    };
    
    var instance1 = new SubType("Nicholas", 29);
    instance1.colors.push("black");
    alert(instance1.colors); //"red,blue,green,black"
    instance1.sayName(); //"Nicholas";
    instance1.sayAge(); //29
    
    var instance2 = new SubType("Greg", 27);
    alert(instance2.colors); //"red,blue,green"
    instance2.sayName(); //"Greg";
    instance2.sayAge(); //27

    組合繼承避免了原型鏈和借用構造函數的缺陷,融合二者之長,是最經常使用的JS繼承模式。

  3. 原型式繼承
    若是隻是想讓一個對象與另外一個對象保持相似的狀況下,沒有必要興師動衆地建立構造函數。咱們可使用原型式繼承。
    Rect.prototype = Object.create(Shape.prototype);

  4. 原型繼承
    JavaScript的原型繼承實現方式就是:

  • 定義新的構造函數,並在內部用call()調用但願「繼承」的構造函數,並綁定this;

  • 藉助中間函數F實現原型鏈繼承,最好經過封裝的inherits函數完成;

  • 繼續在新的構造函數的原型上定義新方法。

    var print = require('./print.js');
    
    function Student(props) {
        this.name = props.name || 'Unnamed';
    }
    
    Student.prototype.hello = function () {
        print('Hello, ' + this.name + '!');
    };
    
    function Sub(props) {
        Student.call(this, props);
        this.grade = props.grade || 1;
    }
    
    Sub.prototype.getGrade = function() {
        return this.grade;
    };
    
    function inherits(Child, Parent) {
        var F = function() {};
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
    }
    
    inherits(Sub, Student);
    
    var xiaoming = new Sub({
        name: 'xiaoming',
        grade: 100
    });
    
    print(xiaoming.name + ' ' + xiaoming.grade);
    
    print(xiaoming instanceof Student);
    print(xiaoming instanceof Sub);

class繼承

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}
class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 記得用super調用父類的構造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

ES6引入的class和原有的JavaScript原型繼承有什麼區別呢?實際上它們沒有任何區別,class的做用就是讓JavaScript引擎去實現原來須要咱們本身編寫的原型鏈代碼。簡而言之,用class的好處就是極大地簡化了原型鏈代碼。
這裏遇到的問題是isPrototypeOf的問題

多重繼承

JavaScript不提供多重繼承功能,即不容許一個對象同時繼承多個對象。可是,能夠經過變通方法,實現這個功能。

function M1() {
  this.hello = 'hello';
}

function M2() {
  this.world = 'world';
}

function S() {
  M1.call(this);
  M2.call(this);
}
S.prototype = M1.prototype;

var s = new S();
s.hello // 'hello'
s.world // 'world'
s instanceof M2
//false

上面代碼中,子類S同時繼承了父類M1和M2。固然,從繼承鏈來看,S只有一個父類M1,可是因爲在S的實例上,同時執行M1和M2的構造函數,因此它同時繼承了這兩個類的方法。

擴展

  1. apply的應用:轉換相似數組的對象

    Array.prototype.slice.apply({0:1,length:1})
    // [1]
    
    Array.prototype.slice.apply({0:1})
    // []
    
    Array.prototype.slice.apply({0:1,length:2})
    // [1, undefined]
    
    Array.prototype.slice.apply({length:1})
    // [undefined]
  2. bind結合call,能夠改寫一些JavaScript原生方法的使用形式

    [1, 2, 3].slice(0, 1)
    // [1]
    
    // 等同於
    
    Array.prototype.slice.call([1, 2, 3], 0, 1)
    // [1]

    call方法實質上是調用Function.prototype.call方法,所以上面的表達式能夠用bind方法改寫。

    var push = Function.prototype.call.bind(Array.prototype.push);
    var pop = Function.prototype.call.bind(Array.prototype.pop);
    
    var a = [1 ,2 ,3];
    push(a, 4)
    a // [1, 2, 3, 4]
    
    pop(a)
    a // [1, 2, 3]
  3. 某個屬性究竟是原型鏈上哪一個對象自身的屬性。

    function getDefiningObject(obj, propKey) {
      while (obj && !{}.hasOwnProperty.call(obj, propKey)) {
        obj = Object.getPrototypeOf(obj);
      }
      return obj;
    }
  4. 獲取實例對象obj的原型對象,有三種方法。

    obj.__proto__
    obj.constructor.prototype
    Object.getPrototypeOf(obj)

    推薦最後一種

    面向對象感受還沒怎麼搞明白,模塊的東西還沒弄明白,有時間補上

參考資料

廖雪峯老師的教程
阮一峯老師的教程
BruceYuj的博客

相關文章
相關標籤/搜索