首先看看下面兩個"1+1=2"的問題:json
var arr = [1]; arr.length = 3; alert(arr); // [1, undefined, undefined]
var outter = "sunshine"; function showScope() { var inner = "darkness"; console.log(outter); //"sunshine" } console.log(typeof inner) // undefined
好了,接下來進入正文。segmentfault
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的屬性。
僅僅經過字面量的方式去建立對象顯然是不現實的,由於當咱們須要建立多個類似的對象時,這樣作會產生大量的重複代碼。須要一種科學的方式去建立對象。
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的屬性,然後者遍歷全部屬性。
這裏給出一種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是解析做用域的函數,它的實現機制大概是:進入函數做用域以前保存當前做用域,新建一個繼承了當前做用域的對象並用它取代當前做用域,解析左括號進入函數做用域並對當前做用域進行擴充,使用擴充後的做用域進行解析,解析右括號離開函數做用域,恢復進入函數前的做用域。
最後說說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中的閉包
上文談到的都是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。