JavaScript学习之ES6 ES2015学记笔记(十二)-类(Class)

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》

阮一峰的网络日志

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值