class在很多面向对象编程的语言中已经很成熟
JS目前面临的问题
用JavaScript新建一个circle类:具有如下功能:
- 在给定的 Canvas 上绘制一个给定圆。
- 跟踪记录生成圆的总数。
- 跟踪记录给定圆的半径,以及如何使其值成为圆的不变条件。
- 计算给定圆的面积
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* Canvas 绘制代码 */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() {
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
Circle.prototype = {
area: function area() {
return Math.pow(this.radius, 2) * Math.PI;
}
};
Object.defineProperty(Circle.prototype, "radius", {
get: function() {
return this._radius;
},
set: function(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。 ");
this._radius = radius;
}
});
上面的代码示例比较繁琐切不符合人的直觉,因此ES6中的class就是要解决这个问题。
方法定义语法
我们需要一种功能类似 obj.prop = method 的新方法来给对象添加“方法”,同时不借助 Object.defineProperty 的力量。人们想要能够简单地实现以下功能:
- 给对象添加标准的函数属性。
- 给对象添加生成器函数属性。
- 给对象添加标准的访问器函数属性。
- 给对象添加任意使用[]语法添加的函数属性,我们称其为预计算(computed)属性名
其中一些功能在以前无法实现,例如:我们不能通过给 obj.prop 赋值来定义 getter或 setter。现在:
var obj = {
// 现在不再使用 function 关键字给对象添加方法
// 而是直接使用属性名作为函数名称。
method(args) { ... },
// 只需在标准函数的基础上添加一个“*”,就可以声明一个生成器函数。
*genMethod(args) { ... },
// 借助|get|和|set|可以在行内定义访问器。
// 只是定义内联函数,即使没有生成器。
// 注意通过这种方式装载的 getter 不能接受参数
get propName() { ... },
// 注意通过这种方式装载的 setter 至少接受一个参数
set propName(arg) { ... },
// []语法可以用于任意支持预计算属性名的地方,来满足上面的第 4 中情况。
// 这意味着你可以使用 symbol,调用函数,联结字符串
// 或其它可以给 property.id 求值的表达式。
// 这个语法对访问器或生成器同样有效,我在这里只是举个例子。
[functionThatReturnsPropertyName()] (args) { ... }
};
利用上述方法重写circle类:
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* Canvas 绘制代码 */ }
Object.defineProperty(Circle, "circlesMade", {
get: function() {
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
Circle.prototype = {
area() {
return Math.pow(this.radius, 2) * Math.PI;
},
get radius() {
return this._radius;
},
set radius(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。 ");
this._radius = radius;
}
};
需要通过定义函数来定义 Circle 类。没有一种方法能够让你在定义函数时就获取它的属性。
类定义语法
需要这样一个系统:给命名构造函数添加方法的同时给函数的.prototype 属性也添加相应方法,从而用这个类构造出的实例也包含相应的方法。既然我们掌握了一种崭新的方法定义语言,一定要物尽其用。在类的所有实例中,只需要一种区分普通函数与特殊函数的方法,在 C++或 Java 中,这种功能对应的关键字是 static。
还需要一个方法,可以在一堆方法中指定出唯一的构造函数。在 C++或 Java中,构造函数与类同名,并且没有返回类型。既然 JS 没有返回类型,需要一个.constructor 属性来支持向后兼容性,可以称之为方法构造函数(methodconstructor)。
综合上述概念再次改写Circle类
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade++;
};
static draw(circle, canvas) {
// Canvas 绘制代码
};
static get circlesMade() {
return !this._count ? 0 : this._count;
};
static set circlesMade(val) {
this._count = val;
};
area() {
return Math.pow(this.radius, 2) * Math.PI;
};
get radius() {
return this._radius;
};
set radius(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。 ");
this._radius = radius;
};
}
关于Circle类的解答:
- 分号是怎么回事? —— 在一次“打造传统类”的尝试中,我们决定编写一个更传统的分隔符。如果不喜欢可以不写,分隔符是可选的。
- 如果我不想要一个构造函数,但是仍然想在创建的对象中放置方法呢? —— 好吧,constructor 方法也是可选的,对象中会默认声明一个空的构造函数 constructor(){}。
- 可以用生成器作为构造函数吗? —— 坚决不可以!构造器不是普通方法,随意添加将会触发类型错误(TypeError),这条规则同样适用于生成器和访问器。
- 我可以用预计算属性名来定义构造函数么? —— 很不幸的是不可以!那将会变得很难预测,所以我们不去尝试。如果你用预计算属性名定义一个方法来命名构造函数,你将得到一个名为 constructor 的方法,它就不是类的构造函数了
- 如果我改变了 Circle 的值,会导致 new Circle 的行为异常么? —— 不会!类与函数表达式类似,会得到一个给定名称的内部绑定,这个绑定不会被外部力量改变,所以无论你在外围作用域给 Circle 变量设置什么值,构造器中的Circle.circlesMade++依然会像预期一样运行
- 但是我可能直接给函数传一个对象字面量作为参数,类是不是就不能实例化了? —— 幸运的是, ES6 中也支持类表达式!可以是命名或匿名表达式,且行为与上述一致,唯一的区别是它们不会在你声明它们的作用域中创建变量。
- 上面提到的可枚举性、可配置性又如何解释呢? —— 人们希望在类中装载的方法是可配置、不可枚举的。一来你可以在对象中装载方法,二来当你枚举对象属性时,不会将装载的方法枚举出来,得到的只是附加的数据属性,这样做是有道理的。
2019-8-16更新
参考阮一峰大神的JavaScript面向对象编程封装。对JavaScript进行面向对象编程比较好的方式是采用构造函数 constructor模式
function vehicle(brand,price){
{
this.brand = brand;
this.price = price;
}
var vehicle1 = new vehicle("audi","1000");
var vehicle2 = new vehicle("bmw","1200");
console.log(vehicle1.brand) //audi
console.log(vehicle2.brand) //bmw
console.log(vehicle1.constructor==vehicle) //true
console.log(vehicle2.constructor==vehicle) //true
console.log(vehicle1 instanceof vehicle) //true
console.log(vehicle2 instanceof vehicle) //true
但是构造函数存在着内存浪费的问题,主要是有新的属性扩充时,当添加速度时:vehicle.speed = 250 每个新建对象都要有这个方法,那就造成了浪费。因此出现了prototype模式
Prototype模式
Javascript规定,每一个构造函数都有一个prototype
属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这个时候就可以通过原型继承的形式:
function vehicle(brand,price){
{
this.brand = brand;
this.price = price;
}
vehicle.prototype.engine = "gasoline";
vehicle.prototype.util= function(){console.log("take a ride;")};
这样每生成一个vehicle 对象就可以实现上面的原型继承,在内存中指向同一块地址。
console.log(vehicle1.engine==vehicle2.engine) //true
prototype的验证
isPropertyOf()
用来验证一个对象与实例之间的关系
console.log(vehicle.prototype.isPropertyOf(vehicle1)) // true
console.log(vehicle.prototype.isPropertyOf(vehicle2)) // true
hasOwnProperty()
每个实例对象都有一个hasOwnProperty()
方法,用来判断某一个属性到底是本地属性,还是继承自prototype
对象的属性。
console.log(vehicle.hasOwnProperty("brand")); //true
console.log(vehicle.hasOwnProperty("engine")); //false
in运算符
in
运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性
console.log("brand" in vehicle1) //true
console.log("util" in vehicle1) //true
对象的继承
1、构造函数绑定
使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function transportation(){
this.type = "汽车";
}
function vehicle(brand,price){
{
transportation.Apply(this,arguments);
this.brand = brand;
this.price = price;
}
var vehicle1 = new vehicle("bmw","1000");
console.log(vehicle1.prototype.type); //汽车
2 prototype模式
如果一个vehicle的prototype对象全部继承自transportation的一个实例,那么vehicle实例可以继承transportation
//将vehicle的对象指向一个transportation的实例
vehicle.prototype = new transportation();
console.log(vehicle.prototype == transportation); //true
console.log(vehilce1.prototype == transportation); //true
//现在
//由于原型链的继承关系 将vehicle的原型对象改为vehicle
vehicle.prototype.constructor = vehicle;
var vehicle1 = new vehicle("bwm","1000");
console.log(vehicle1.type); //汽车
//每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性
console.log(vehicle1.constructor == vehicle.prototype.constructor); //true
3 直接继承prototype
由于transportation对象中,不变的属性都可以直接写入transportation.prototype。所以,我们也可以让vehilce()跳过 transportation(),直接继承transportation.prototype。
function transportation(){ }
transportation.prototype.type= "汽车";
然后,将vehicle的prototype对象,然后指向transportation的prototype对象,这样就完成了继承。
vehicle.prototype = transportation.prototype;
vehicle.prototype.constructor = vehicle;
var vehicle1= new vehicle("bmw","1000");
console.log(vehicle1.type); // 汽车
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立transportation的实例了),比较省内存。缺点是 vehicle.prototype和transportation.prototype现在指向了同一个对象,那么任何对vehicle.prototype的修改,都会反映到transportation.prototype。
vehicle.prototype.constructor = vehicle;
这一句实际上把transpotation.prototype对象的constructor属性也改掉了!
console.log(transportation.prototype.constructor); // vehicle
4 利用空对象做中介
由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。
var F = function(){};
F.prototype = transportation.prototype;
vehicle.prototype = new F();
vehicle.prototype.constructor = vehicle;
F是空对象,所以几乎不占内存。这时,修改vehicle的prototype对象,就不会影响到transportation的prototype对象。
console.log(transportation.prototype.constructor); // transportation
我们将上面的方法,封装成一个函数,便于使用。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用的时候,方法如下
extend(vehicle,transportation);
var vehicle1 = new vehicle("bmw","1000");
alert(vehicle1.type); // 汽车
另外,说明一点,函数体最后一行
Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
5 拷贝继承
法方法与4 类似 就是将父对象的的属性全部拷贝进子对象
首先将transportation不变的属性放到它的prototype对象
function transportation(){}
transportation.prototype.type = "汽车"
然后再用函数实现属性拷贝
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
这样就实现了子对象继承父对象的属性。
本文参考:
《ES6-In-Depth》