9.1面向对象编程
OOP的基础理念非常简单直观:对象是一个逻辑相关的数据和功能的集合。
它以人类对世界的自然理解为设计理念。
类指的是通用的东西(模板)。实例(或者对象实例)指具体的东西。人类==》郭靖
9.2创建类和实例
在ES6之前,JavaScript 中创建类的过程比较繁琐且不直观。ES6 新引入了一些创
建类的便捷语法:
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
this.userGears = ['P', 'N', 'R', 'D'];
this.userGear = this.userGears[0];
}
shift(gear) {
if (this.userGears.indexOf(gear) < 0) {
throw new Error(`Invalid gear ${gear}`);
}
this.userGear = gear;
}
}
const car1 = new Car("Tesla", "ModelS");
const car2 = new Car("Mazda", "3i");
console.log(car1 instanceof Car);
console.log(car2 instanceof Car);
car1.shift('D');
car2.shift('R');
console.log(car1.userGear);
console.log(car2.userGear);
9.3类即函数
类实际上就是函数。在ES5中,会这样编写Car类
ES6中并没有什么新东西,仅仅是多出了一些更易用的新语法,
//构造方法 始终以大写字母开头
function Car(make, model) {
this.make = make;
this.model = model;
this._userGears = ['P', 'N', 'R', 'D'];
this._userGear = this._userGears[0];
}
//使用关键字new来创建一个新的实例对象
let car = new Car("Tesla","ModelS");
//验证方式
class ES6Car {}
function ES5Car() {}
console.log(typeof ES6Car);
console.log(typeof ES5Car);
9.4遍历属性(枚举属性)
对象属性枚举是无序的
const SYM = Symbol("SYM");
const o = {
a: 1,
b: 2,
c: 3,
[SYM]: 4
};
for (let prop in o) {
if (!o.hasOwnProperty(prop)) {
continue;
}
console.log(`${prop}:${o[prop]}`);
}
Object.keys(o).forEach(prop => console.log(`${prop}:${o[prop]}`));
let syms = Object.getOwnPropertySymbols(o);
console.log(syms);
for(let i=0;i<syms.length;i++){
console.log(syms[i].description);
console.log(o[syms[i]]);
}
Symbol属性遍历
for in不能遍历到Symbol类型的属性
for of不能遍历到Symbol类型的属性
Object.keys()不能遍历到Symbol类型的属性
Object.getOwnPropertySymbols(obj)仅遍历obj的Symbol类型的属性
9.5动态属性(get/set)
大部分面向对象语言都会通过封装来阻止直接赋值对象属性赋值。
JavaScript没有访问权限机制,动态属性可以稍微有效地弥补这种不足
私有属性加上下划线作为前缀,是一种约定俗成的做法
get/set关键字 关键字将对象属性与函数进行绑定,当属性被访问时,对应函数被执行。
表示即时调用
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
this._userGears = ['P', 'N', 'R', 'D'];
this._userGear = this._userGears[0];
}
get userGear() {
return this._userGear;
}
set userGear(value) {
if (this._userGears.indexOf(value) < 0) {
throw new Error(`Invalid gear ${value}`);
}
this._userGear = value;
}
shift(gear) {
this.userGear = gear;
}
}
const car1 = new Car("Tesla", "ModelS");
const car2 = new Car("Mazda", "3i");
console.log(car1 instanceof Car);
console.log(car2 instanceof Car);
car1.shift('D');
car2.shift('R');
console.log(car1.userGear);
console.log(car2.userGear);
WeakMap
即时调用函数将WeakMap隐藏在一个闭包内,从而阻止了外界的访问。WeakMap可以安全地存储任何不想被Car类外部访问的属性。
const Car = (function() {
const carProps = new WeakMap();
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
this._userGears = ['P', 'N', 'R', 'D'];
carProps.set(this, {
userGear: this._userGears[0]
});
}
get userGear() {
return carProps.get(this).userGear;
}
set userGear(value) {
if (this._userGears.indexOf(value) < 0) {
throw new Error(`Invalid gear ${value}`);
}
carProps.get(this).userGear = value;
}
shift(gear) {
this.userGear = gear;
}
}
return Car;
})();
const car1 = new Car("Tesla", "ModelS");
const car2 = new Car("Mazda", "3i");
console.log(car1 instanceof Car);
console.log(car2 instanceof Car);
car1.shift('D');
car2.shift('R');
console.log(car1.userGear);
console.log(car2.userGear);
9.6原型和原型链
大多数时候,不必搞懂原型链和动态调度的机制。但在不同的阶段,可能会遇到一
些迫使大家去深入学习它们的问题。最好在学习的时候能都明白其中的细节。
function ES5Car() {
this.make = "Tesla";
//实例方法
this.shift = function() {
console.log('当前类挂挡');
}
}
let car1 = new ES5Car();
let car2 = new ES5Car();
console.log(car1.shift ===car2.shift);
class ES6Car {
constructor(make, model) {
this.make = make;
this.model = model;
this.userGears = ['P', 'N', 'R', 'D'];
this.userGear = this.userGears[0];
}
//原型方法
shift(gear) {
if (this.userGears.indexOf(gear) < 0) {
throw new Error(`Invalid gear ${gear}`);
}
this.userGear = gear;
}
}
let car3 = new ES6Car("Tesla", "ModelS");
let car4 = new ES6Car("Mazda", "3i");
console.log(car3.shift===car4.shift);
对应名称
prototype :原型
proto :原型链
从属关系
每个函数都有一个叫作prototype(原型) 的特殊属性
每个对象都有一个叫作proto(原型链)的特殊属性
对象的proto保存着该对象的构造函数的prototype
function Car() {
}
//prototype -> 函数的一个属性:值是对象
console.log(Car.prototype);
const car1 = new Car();
//__proto__ -> 对象Object的一个属性:值是对象
console.log(car1.__proto__);
//对象的__proto__保存着该对象的构造函数的prototype
console.log(car1.__proto__ === Car.prototype);
//
console.log(Car.prototype.__proto__);
//
console.log(Car.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__); //Object最顶层__proto__ null
关于原型,有一个重要的机制叫作动态调度
当试图访问对象的某个属性或者方法时,如果它不存在于当前对象中,JavaScritp会检查它是否存在于对象原型中。因为同一个类的所有实例共用同一个原型,如果原型中存在某个属性或者方法,则该类的所有实例都可以访问这个属性或方法。
function Car() {
this.make = "Tesla";
this.shift = function(){
console.log('当前类挂挡');
}
}
console.log(Car.prototype);
Car.prototype.mode = "ModelS";
const car1 = new Car();
console.log(car1);
Object.prototype.userGears = ['P', 'N', 'R', 'D'];
console.log(car1.make);
console.log(car1.mode);
console.log(car1.userGears);
// Car {
// make:"Tesla",
// __proto__:Car.prototype = {
// mode:"ModelS",
// __proto__:Object.prototype = {
// userGears:['P', 'N', 'R', 'D'],
// }
// }
// }
Car.prototype.shift = function(){
console.log('prototype原型挂挡');
}
//动态调度
car1.shift();
9.7继承
在分析原型的时候,已经看到了继承的身影:当创建一一个类的实例时,它继承了类
原型中所有的功能。尽管如此,它并没有就此打住:如果一个方法没有在对象原型
中找到其定义,它会检查原型的原型。这样就建立了一个原型链。JavaScript 会沿
着原型链走下去,直到某个原型满足了需求。如果找不到这样的原型,程序最终会报错
//交通工具
class Vehicle{
constructor() {
this.passengers = [];
console.log("Vehicle Create");
}
addPassenger(p){
this.passengers.push(p);
}
}
//汽车 继承 交通工具
class Car extends Vehicle{
constructor() {
super();
console.log("Car created");
}
deployAirbags(){
console.log("BWOOSH");
}
}
const v = new Vehicle();
v.addPassenger("Frank");
v.addPassenger("Judy");
console.log(v.passengers.join(","));
const c = new Car();
c.addPassenger("Alice");
c.addPassenger("Cameron");
console.log(c.passengers.join(","));
c.deployAirbags();
// 注意,可以在c上调用deployAirbags,但是不能在v上调用。换言之,继承
// 是单向的。Car类的实例可以访问所有Vehicle类的方法,反之却不行。
//v..deployAirbags();//报错
9.8静态方法
静态方法(也叫类方法),它不与实例绑定。在静态方法中,this绑
定的是类本身,但通常使用类名来代替this是公认的最佳实践。
class Car {
static getNextVin() {
return Car.nextVin++;
}
constructor(make, model) {
this.make = make;
this.model = model;
this.vin = Car.getNextVin();
}
//判断辆车是否具有相同的品牌和型号
static areSimilar(car1, car2) {
return car1.make === car2.make && car1.mode === car2.model;
}
//判断辆车是否具有相同VIN
static areSame(car1, car2) {
return car1.vin === car2.vin;
}
}
Car.nextVin = 0;
const car1 = new Car("Tesla", "s");
const car2 = new Car("Mazda", "3");
const car3 = new Car("Mazda", "3");
console.log(car1.vin);
console.log(car2.vin);
console.log(car3.vin);
Car.areSimilar(car1, car2);
Car.areSimilar(car2, car3);
Car.areSame(car2, car3);
Car.areSame(car2, car3);
9.9多态
多态是面向对象的一个术语,意思是一个实例不仅是它自身类的实例,也可以被当做它的任何父类的实例来使用。
JavaScript弱类型语言,就是数据类型可以被忽略的语言,天生具有多态性。
JavaScript提供了instanceof运算符,它会指出某个对象是否属于某个给定类。
class Vehicle{}
class Motorcycle extends Vehicle {}
class Car extends Vehicle {}
const c = new Car();
const m = new Motorcycle();
console.log(c instanceof Car);
console.log(c instanceof Vehicle);
console.log(m instanceof Car);
console.log(m instanceof Motorcycle);
console.log(m instanceof Vehicle);
function run(obj){
if(obj instanceof Car){
console.log("汽车对象,执行汽车的方法");
}
if(obj instanceof Motorcycle){
console.log("摩托车对象,执行汽车的方法");
}
}
9.10hasOwnProperty()和toString()方法
class Super {
constructor() {
this.name = 'Super';
this.isSuper = true;
}
}
//合法,但不推荐这么做
Super.prototype.sneaky = 'not recommended';
class Sub extends Super {
constructor() {
super();
this.name = 'Sub';
this.isSub = true;
}
}
const obj = new Sub();
// 如果obj中存在x属性,obj . hasOwnProperty(x)返回true。反之,如果属性
// 没有定义或者定义在原型链中,obj .hasOwnProperty (x)结果为false。
for (let p in obj) {
console.log(`${p}:${obj[p]}`
+(obj.hasOwnProperty(p)?'':'(inherited)'));
}
// name,isSuper 和isSub三个属性都被定义在实例中,而不是在原型链中,
// 属性sneaky被手动添加到父类的原型中。
// 使用object. keys就可以完全避免这个问题,因为object.keys只包含了原型中定义的属性。
每个对象最终都是继承自object类,所以可以在object中使用的方法在任何,其他对象中都可以用。其中的一个方法是toString,该方法是为了给对象提供一个默认的字符串表示。toString默认的方法会返回”[object object]”, 这
种表示并没有什么实际用处。
class Car {
static getNextVin() {
return Car.nextVin++;
}
constructor(make, model) {
this.make = make;
this.model = model;
this.vin = Car.getNextVin();
}
toString() {
return `${this.make} ${this.model}:${this.vin}`;
}
}
Car.nextVin = 0;
let car = new Car("Tesla", "s");
console.log(car.toString());
9.11多继承
一些面向对象语言(C++)支持多继承,也就是说一个类可以有两个直接的父类。多继承有引发冲突的风险。
比如两个父类都有一个同名的方法,那么子类该继承谁的方法呢?所以很多编程语言选择单继承来避免这个问题。但是 当思考现实世界的问题时,多继承往往是有意义的。例如,汽车可能继承自交通工具和“可接受保险的”。不支持多继承的编程语言通常会引人接口的概念来绕过这个问题。
一个类(Car)只能有一个父类(Vehicle),但它可以有多个接口(Insurable,Container等等)。
JavaScript是一种有趣的混合。技术上,它是一一个单继承的语言,因为在原型链并
不会去寻找多个父类,但是它确实提供了一些方法,有时候这些方法比多继承或者
接口还要好用。
class InsurancePolicy {
}
class Car{
}
function makeInsurable(o) {
o.addInsurancePolicy = function(p) {
this.insurancePolicy = p;
}
o.getInsurancePolicy = function() {
return this.insurancePolicy;
}
o.isInsured = function() {
return !!this.insurancePolicy;
}
}
const car1 = new Car();
makeInsurable(car1);
car1.addInsurancePolicy(new InsurancePolicy())