ES5-ES6-ES7_class類
傳統創建對象模板的方式
JavaScript 語言中,生成實例對象的傳統方法是通過構造函數
//JavaScript 語言中,生成實例對象的傳統方法是通過構造函數 function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; }; var p = new Point(1, 2); console.log(p.toString());//(1, 2)
ES6創建對象模板的方式Class
ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類
基本上,ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已
ES6 的類,完全可以看作構造函數的另一種寫法
//ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過class關鍵字,可以定義類 class Point { constructor(x, y) {//構造方法,而this關鍵字則代表實例對象 this.x = x; this.y = y; } //toString方法。註意,定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函數定義放進去了就可以了。 // 另外,方法之間不需要逗號分隔,加了會報錯 toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } const p = new Point(1,2); //類必須使用new調用,否則會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也可以執行console.log(p.toString()); //(1, 2) console.log(typeof Point) //function,類的數據類型就是函數,類本身就指向構造函數 console.log(Point === Point.prototype.constructor) // true
constructor 方法
constructor方法是類的默認方法,通過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被默認添加。
//定義了一個空的類Point,JavaScript 引擎會自動為它添加一個空的constructor方法 class Point { } // 等同於 class Point { constructor() {} }
class Foo { constructor() { //constructor方法默認返回實例對象(即this),完全可以指定返回另外一個對象 return Object.create(null); //constructor函數返回一個全新的對象,結果導致實例對象不是Foo類的實例 } } console.log(new Foo() instanceof Foo); // false
類的實例對象
根據類創建實例對象——類必須使用new調用,否則會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也可以執行
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } const p = new Point(1,2); //類必須使用new調用,否則會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也可以執行 console.log(p.toString()); //(1, 2)
實例對象屬性與原型對象屬性——實例的屬性除非顯式定義在其本身(即定義在this對象上),否則都是定義在原型上(即定義在class上)
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } const p = new Point(1,2); console.log(p.toString()); //(1, 2) //x和y都是實例對象point自身的屬性(因為定義在this變量上)不是原型對象的,所以hasOwnProperty方法返回true console.log(Point.hasOwnProperty(‘x‘)) // true console.log(Point.hasOwnProperty(‘y‘))// true console.log(Point.__proto__.hasOwnProperty(‘y‘))// false //toString是原型對象的屬性(因為定義在Point類上),不是實例對象的,所以hasOwnProperty方法返回false console.log(Point.hasOwnProperty(‘toString‘)) // false console.log(Point.__proto__.hasOwnProperty(‘toString‘)) // true
通過__proto__屬性為‘類’添加方法
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } var p1 = new Point(2,3); var p2 = new Point(3,2); //p1和p2都是Point的實例,它們的原型都是Point.prototype,所以__proto__屬性是相等的 console.log(p1.__proto__ === p2.__proto__) //true //可以通過實例的__proto__屬性為“類”添加方法 p1.__proto__.printName = function () { return ‘Oops‘ }; console.log(p1.printName()) // "Oops" console.log(p2.printName()) // "Oops" var p3 = new Point(4,2); console.log(p3.printName()) // "Oops" //p1的原型上添加了一個printName方法,由於p1的原型就是p2的原型,因此p2也可以調用這個方法。 // 而且,此後新建的實例p3也可以調用這個方法。 // 這意味著,使用實例的__proto__屬性改寫原型,必須相當謹慎,不推薦使用,因為這會改變“類”的原始定義,影響到所有實例
立即執行的類的實例
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }(‘張三‘); person.sayName(); // "張三"
使用表達式的形式定義類
const MyClass = class Me { getClassName() { return Me.name; } }; //使用表達式定義了一個類。需要註意的是,這個類的名字是MyClass而不是Me,Me只在 Class 的內部代碼可用,指代當前類 const p = new MyClass(); console.log(p.getClassName()); //Me,Me只在 Class 內部有定義。
變量提升——類不存在變量提升(hoist), ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之後定義
//Foo類使用在前,定義在後,這樣會報錯, // 因為 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之後定 new Foo(); // ReferenceError: Foo is not defined class Foo {}
私有方法
私有方法是常見需求,但 ES6 不提供,只能通過變通方法模擬實現
私有方法模擬實現方式一——在命名上加以區別,方法前面的下劃線,表示這是一個只限於內部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調用到這個方法
class Widget { foo (baz) { // 公有方法 this._bar(baz); } _bar(baz) { // 私有方法,_bar方法前面的下劃線,表示這是一個只限於內部使用的私有方法 return this.snaf = baz; } } const w = new Widget(); console.log(w._bar(‘fffffff‘)) //fffffff,這種命名是不保險的,在類的外部,還是可以調用到這個方法
私有方法模擬實現方式二————將私有方法移出模塊,因為模塊內部的所有方法都是對外可見的
class Widget { foo (baz) { bar.call(this, baz); } } function bar(baz) { return this.snaf = baz; } //foo是公有方法,內部調用了bar.call(this, baz)。這使得bar實際上成為了當前模塊的私有方法 const w = new Widget(); console.log(w.foo(‘fffff‘))
Class的繼承
Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
子類必須在constructor方法中調用super方法,否則新建實例時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然後對其進行加工。如果不調用super方法,子類就得不到this對象
ES6 的繼承機制實質是先創造父類的實例對象this(所以必須先調用super方法),然後再用子類的構造函數修改this
如果子類沒有定義constructor方法,這個方法會被默認添加,也就是說,不管有沒有顯式定義,任何一個子類都有constructor方法。
另一個需要註意的地方是,在子類的構造函數中,只有調用super之後,才可以使用this關鍵字,否則會報錯。這是因為子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } class ColorPoint extends Point { constructor(x, y, color) { //this.color = color; //在調用super之前,this是不存在的,所以會報錯 super(x, y); // 調用父類的constructor(x, y), this.color = color; } toString() { return this.color + ‘ ‘ + super.toString(); // 調用父類的toString() } } const cp = new ColorPoint(1,2,‘red‘); console.log(Object.getPrototypeOf(ColorPoint)); //[Function: Point] console.log(Object.getPrototypeOf(ColorPoint) === Point); //true
通過繼承創建的實例對象所屬的類的實例——實例對象cp同時是ColorPoint和Point兩個類的實例
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } class ColorPoint extends Point { constructor(x, y, color) { //this.color = color; //在調用super之前,this是不存在的,所以會報錯 super(x, y); // 調用父類的constructor(x, y), this.color = color; } toString() { return this.color + ‘ ‘ + super.toString(); // 調用父類的toString() } } const cp = new ColorPoint(1,2,‘red‘); console.log(cp instanceof Point); // true console.log(cp instanceof ColorPoint); // true
Object.getPrototypeOf()——Object.getPrototypeOf方法可以用來從子類上獲取父類,可以使用這個方法判斷,一個類是否繼承了另一個類
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } class ColorPoint extends Point { constructor(x, y, color) { //this.color = color; //在調用super之前,this是不存在的,所以會報錯 super(x, y); // 調用父類的constructor(x, y), this.color = color; } toString() { return this.color + ‘ ‘ + super.toString(); // 調用父類的toString() } } const cp = new ColorPoint(1,2,‘red‘); console.log(Object.getPrototypeOf(ColorPoint)); //[Function: Point] console.log(Object.getPrototypeOf(ColorPoint) === Point); //true
super關鍵字
super作為函數調用時,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次super函數
super雖然代表了父類的構造函數,但是返回的是子類的實例,即super內部的this指的是子類,因此super()在這裏相當於父類.prototype.constructor.call(this)。
class Point { constructor(x, y) { this.x = x; this.y = y; console.log(new.target.name); } toString() { return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘; } } class ColorPoint extends Point { constructor(x, y, color) { //子類的構造函數之中的super(),代表調用父類的構造函數。這是必須的,否則 JavaScript 引擎會報錯 super(x, y); // 調用父類的constructor(x, y), this.color = color; } toString() { //super(); //SyntaxError: ‘super‘ keyword unexpected here
// 作為函數時,super()只能用在子類的構造函數之中,用在其他地方就會報錯
return this.color + ‘ ‘ + super.toString(); // 調用父類的toString() } } const cp = new ColorPoint(1,2,‘red‘); //ColorPoint,創建子類對象的時候,this指向的是子類 console.log(cp.toString()) // red (1, 2)
super作為對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類
由於super指向父類的原型對象,所以定義在父類實例上的方法或屬性,是無法通過super調用的
如果屬性定義在父類的原型對象上,super就可以取到
通過super調用父類的方法時,super會綁定子類的this
class Point { constructor() { this.x = 1; } toString() { return ‘(‘ + this.x + ‘)‘; } } Point.prototype.z = 200; class ColorPoint extends Point { constructor() { super(); this.x = 3 } toString() { // 調用父類的toString(),通過super調用父類的方法時,super會綁定子類的this return super.toString(); } } const cp = new ColorPoint(); console.log(cp.toString()) //(3)
ES5-ES6-ES7_class類