JavaScript面向對象那些事

首先看看下面兩個"1+1=2"的問題:json

問題一:爲何改變length的值,數組的內容會變化?

var arr = [1];
arr.length = 3;
alert(arr);   // [1, undefined, undefined]

問題二:爲何在showScope函數內能訪問outter,在函數外不能訪問inner?

var outter = "sunshine";

function showScope() {
    var inner = "darkness";
    
    console.log(outter);    //"sunshine"
}

console.log(typeof inner)   // undefined

好了,接下來進入正文。segmentfault

1、對象的屬性

var person = {
    name: "Simon",
    _age: 21,
    isYoung: true,
    friends: ["Johnny", "Carlton", "Amy"],
    sayName: function() {
        console.log(this.name);
    }
    educate: {
        primarySch: "",
        highSch: "",
        university: ""
    }
};

上面的person對象是JS對象的字面量形式,本質上是一個鍵值對的無序集合,這些鍵值 對叫作屬性。屬性的名稱只能是字符串形式的,而屬性的值能夠是字符串、數字、布爾值等基本類型,也能夠是數組、函數、對象等引用類型。值得一提的是,若是屬性的名稱是JS可以識別的標識符,如name、first_name、$name,則在定義屬性時不用像json那樣爲屬性名加上引號;但屬性名稱是first-name這種JS沒法識別的標識符時,就須要爲其加上引號了。這兩種狀況也會形成訪問方式不一樣,前者既能夠經過person.first_name的形式訪問,也能夠經過person[first_name]的形式訪問。但後者只能經過中括號的形式訪問。數組

若是要對屬性分類的話,屬性能夠分爲兩類:數據屬性、訪問器屬性。這兩種屬性都分別有着一些特性:閉包

數據屬性
  • Configurable: 可否修改或刪除屬性,默認爲true;dom

  • Enumerable: 可否經過for-in循環遍歷屬性,默認爲true;函數

  • Writable: 可否修改屬性的;this

  • Value: 存放屬性的值,默認爲 undefined;spa

訪問器屬性
  • Configurable: 同上;prototype

  • Enumerable: 同上;設計

  • Get: 在讀取屬性的值時調用的函數;

  • Set: 在設置屬性的值時調用的函數;

這些特性沒法直接訪問,但能夠經過Object.defineProperty(obj, attr, descriptor)函數定義這些特性。
基於上面的person對象各舉一個例子:

// 數據屬性
Object.defineProperty(person, "name", {
    configurable: false
})

console.log(person,name); // Simon
person.name = "zai";
console.log(person,name); // Simon

//訪問器屬性
Object.defineProperty(person, "age", {
    get: function() {
        return this._age;
    },
    set: function(newValue) {
    
        if (newValue > 30) {
            this._age = newValue;
            this.isYoung = false;
        }
    }
})

到這裏第一個問題就獲得瞭解決,數組的length屬性其實就是一種訪問器屬性。

此外操做屬性的方法還有:Object.defineProperties 用來一次定義多個屬性,Object.getOwnPropertyDescriptor(obj, attr) 用來讀取屬性的特性。另外能夠經過delete操做符去刪除Configurable值爲true的屬性。

2、如何建立對象

僅僅經過字面量的方式去建立對象顯然是不現實的,由於當咱們須要建立多個類似的對象時,這樣作會產生大量的重複代碼。須要一種科學的方式去建立對象。

function Person(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
    // this.prototype = { constructor: this };
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
}

Person.prototype.sayAge = function() {
    console.log(this.age);
};

var simon = new Person("Simon", 22, ["Amy", "Johnny", "Carlton"]);
simon.sayName();   //委託

上面的代碼結合了構造函數和原型兩種方式去建立對象,首先聊聊構造函數:

構造函數

構造函數本質上仍是函數,只不過爲了區分將其首字母大寫了而已。注意註釋掉的代碼是自動執行的,但這並非構造函數獨有的,每一個函數在聲明時都會自動生成prototype。構造函數不同的地方在於它的調用方式——new,new調用構造函數的大體過程:

  • 產生一個新對象;

  • 將構造函數的做用域賦給新對象;

  • 執行構造函數中的代碼;

  • 返回新對象或者指定返回的對象;

構造函數本質上還是函數,因此固然能夠直接調用,這樣構造函數中的this就指的是全局對象,顯然不符合預期。

原型

《JavaScript高級程序設計》上的一幅圖很好的解釋了原型、構造函數、實例之間的關係:
圖片描述

執行simon.sayName( )時,首先在simon對象自己的做用域中尋找sayName,沒有找到以後再去其原型Person.prototype中尋找,這個過程叫作委託。那麼問題就來了,當咱們不知道一個對象的構成時,如何去判斷一個屬性屬於對象仍是其原型呢?obj.hasOwnProperty(propName)就是作這個事情的函數,經常被用在for-in循環遍歷對象的屬性的過程當中,與for-in相似的兩個方法:Object.keys(obj)、Object.getOwnPropertyNames(obj) 這兩個方法返回的都是屬性名的數組,都不包括原型中的屬性,區別在於前者和for-in同樣只遍歷enumrable爲 true的屬性,然後者遍歷全部屬性。

3、繼承

這裏給出一種JavaScript實現繼承的方式:

function Vehicle(maxSpeed, wheels) {
    this.maxSpeed = maxSpeed;
    this.wheels = wheels;
}

Vehicle.prototype.checkMaxSpeed = function() {
    console.log(this.maxSpeed);
};

function Car(brand, maxSpeed) {
    Vehicle.call(this, maxSpeed, 4);
    this.brand = brand;
}

Car.prototype = new Vehicle();
Car.prototype.constructor = Car;
Car.prototype.checkBrand = function() {
    console.log(this.brand);
};

var panemera = new Car("Panemera", 250);

這裏的關鍵在於在Car中調用Vehicle,向父類構造器傳遞參數,初始化子類的屬性,再進行擴充(brand),固然僅僅有構造函數仍是不行的,還須要原型鏈才能更好地實現繼承,這裏Car的原型是Vehicle的一個實例,值得注意的是Car.prototype = new Vehicle();以後,本來的constructor丟失了,新的constructor在這裏指向了Vehicle,須要重置爲Car。

以前提出的第二個問題其實就是用繼承來實現的:

function showScope() {
    // scope表明當前做用域
    var oldScope = scope;
    var Scope = function() {};
    
    //繼承當前做用域
    Scope.prototype = scope;
    scope = new Scope();
    
    // 進入函數做用域,擴充做用域
    advance("{");
    parse(scope);     // 用當前做用域作解析
    advance("}");
   
   scope =oldScope;
}

假設showScope是解析做用域的函數,它的實現機制大概是:進入函數做用域以前保存當前做用域,新建一個繼承了當前做用域的對象並用它取代當前做用域,解析左括號進入函數做用域並對當前做用域進行擴充,使用擴充後的做用域進行解析,解析右括號離開函數做用域,恢復進入函數前的做用域。

4、私有成員的實現

最後說說JavaScript中私有成員的實現,一個頗有趣的例子:

function AladdinLamp() {
    var limit = 3;
    
    function rubLamp() {
        if (limit > 0) {
            limit -= 1;
            return true;
        } else {
            return false;
        }
    }
    
    this.satisfyWish = function() {
        return rubLamp() ? Math.random() : null;
    };
}

這裏的limit和rubLamp都是AladdinLamp的私有成員,沒法從外部直接訪問,只能經過惟一暴露出來的satisfyWish調用,這其實是一種閉包,關於閉包請參考本專欄中的淺談JavaScript中的閉包

5、ES6中的類與繼承

上文談到的都是ES5,那麼ES6有什麼不一樣呢,先來看看ES6中的類:

class Vehicle {
    constructor(maxSpeed, wheels) {
        this.maxSpeed = maxSpeed;
        this.wheels = wheels;
    }
    
    checkMaxSpeed() {
        console.log(this.maxSpeed);
    }
    
    static openDoor() {
        console.log("Welcome");
    }
}

Vehicle.length = 100;

let bike = new Vehicle(40, 2);
// TypeError
bike.openDoor();

不一樣之處在於構造函數換成了Class,其實Class本質上也是函數,constructor就至關於ES5中的構造函數,而直接在類中聲明的checkMaxSpeed實際至關於 Vehicle.prototype.checkMaxSpeed = ...
有意思的是ES6中多了靜態方法的實現,這裏的openDoor沒法在實例中調用,能夠經過Vehicle.openDoor直接調用,能夠繼承給子類。另外經過Vehicle.props = ...的形式能夠定義靜態變量。最後注意Vehicle只能經過new調用,不然會報錯,是由於在constructor中檢測了new.target。
再看看ES6中的繼承:

class Car extends Vehicle {
    constructor(maxSpeed, wheels, brand) {
        super(maxSpeed, wheels);
        this.brand = brand;
    }
    
    checkBrand() {
        console.log(this.brand);
    }
}

繼承的關鍵在於constructor中調用了super,即父類的構造函數。這裏必定要調用super,由於子類的this是由super建立的,以後再去擴充this。

相關文章
相關標籤/搜索