JavaScript 面向对象编程
面向对象编程 Object Oriented Programming
面向对象编程用对象把数据和方法聚合起来。
面向对象编程的优点
- 能写出模块化的代码
- 能使得代码更灵活
- 能提高代码的可重用性
面向对象编程的原则
- 继承(inheritance):子类/派生类从父类/基类/超类中派生,形成继承结构
- 封装(encapsulation):代码的实现对用户不可见,例如调用
toUpperCase()
,直接调用即可,不用考虑函数内部的实现 - 抽象(abstraction):抽象就是抽象出你正在尝试做的事情的概念,而不是处理这个概念的具体表现形式
- 多态(polymorphism):多种形态
对象
创建对象
- 使用对象字面量创建:
let dog = {
name: "Mit",
numLegs: 2
}
- 使用
Object.create()
创建:(所有的对象都来自某个类,对象是类的实例,也是一种层级结构)
class Animal {
/* code */
}
var myDog = Object.create(Animal);
console.log(Animal);
- 使用
new
关键字:(当使用默认或者空构造函数方法,JS 会隐式调用对象的超类来创建实例)
class Animal { /* code here */ }
var myDog = new Animal();
console.log(Animal)
访问对象属性
let dog = {
name: "Mit",
numLegs: 2
}
console.log(dog.name); // Mit
对象方法 Method
对象可以有一个叫做 method 的特殊属性。
方法属性也就是函数。 这给对象添加了不同的行为。
let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {
return "This dog has 4 legs."
}
};
dog.sayLegs();
this
this 代表上下文环境中引用的对象。
let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
};
dog.sayLegs(); // This dog has 4 legs.
构造函数
constructors 是创建对象的函数。 函数给这个新对象定义属性和行为。 可将它们视为创建的新对象的蓝图。
构造函数遵循一些惯例规则:
- 构造函数函数名的首字母大写,这是为了方便区分构造函数(
constructors
)和其他非构造函数。 - 构造函数使用
this
关键字来给它将创建的这个对象设置新的属性。 在构造函数里面,this
指向的就是它新创建的这个对象。 - 构造函数定义了属性和行为就可创建对象,而不是像其他函数一样需要设置返回值。
function Dog() {
this.name = "N";
this.color = "red";
this.numLegs = 2;
}
function Dog(name, color) {
this.name = name;
this.color = color;
this.numLegs = 4;
}
let terrier = new Dog("fsd","fa");
JS 中有很多内置对象类型,称为“原生对象(native object)”。有些类型可以使用构造函数获取一个实例,但是有些类型没有也不需要构造函数。例如 Date
可以用 new
关键字创建对象,但是 Math
对象就不行。
如果对象类型是基本类型(primitive),使用构造函数通常不是最佳实践:
let apple = new String('apple');
apple; // string
对于常规对象,最好使用对象字面量语法 {}
,而不是 new
。例如:数组,函数,正则表达式等。
下列类型可以考虑构造函数:
new Date();
new Error();
new Map();
new Promise();
new Set();
new WeakSet();
new WeakMap();
使用构造函数创建对象
注意: 构造函数内的 this
总是指被创建的对象。
注意:通过构造函数创建对象的时候要使用 new
操作符。
function Dog() {
this.name = "Rupert";
this.color = "brown";
this.numLegs = 4;
}
let hound = new Dog();
instanceof
凡是通过构造函数创建出的新对象,这个对象都叫做这个构造函数的 instance。 JavaScript 提供了一种很简便的方法来验证这个事实,那就是通过 instanceof
操作符。 instanceof
允许将对象与构造函数之间进行比较,根据对象是否由这个构造函数创建的返回 true
或者 false
。 以下是一个示例:
let Bird = function(name, color) {
this.name = name;
this.color = color;
this.numLegs = 2;
}
let crow = new Bird("Alexis", "black");
crow instanceof Bird; // true
属性
自身属性 Own Property
请看下面的实例,Bird
构造函数定义了两个属性:name
和 numLegs
:
function Bird(name) {
this.name = name;
this.numLegs = 2;
}
let duck = new Bird("Donald");
let canary = new Bird("Tweety");
name
和 numLegs
被叫做 自身属性,因为它们是直接在实例对象上定义的。 这就意味着 duck
和 canary
这两个对象分别拥有这些属性的独立副本。 事实上,Bird
的所有实例都将拥有这些属性的独立副本。
原型属性 Prototype Property
所有 Bird
实例可能会有相同的 numLegs
值,所以在每一个 Bird
的实例中本质上都有一个重复的变量 numLegs
。
当只有两个实例时可能并不是什么问题,但想象一下如果有数百万个实例。 这将会产生许许多多重复的变量。
更好的方法是使用 Bird
的 prototype
。 prototype
是一个可以在所有 Bird
实例之间共享的对象。 以下是一个在 Bird prototype
中添加 numLegs
属性的示例:
Bird.prototype.numLegs = 2;
现在所有的 Bird
实例都拥有了共同的 numLegs
属性值。
console.log(duck.numLegs);
console.log(canary.numLegs);
由于所有的实例都可以继承 prototype
上的属性,所以可以把 prototype
看作是创建对象的 "配方"。 请注意:duck
和 canary
的 prototype
属于 Bird
的构造函数,即 Bird 的原型 Bird.prototype
。 JavaScript 中几乎所有的对象都有一个 prototype
属性,这个属性是属于它所在的构造函数。
迭代属性 Iterate Property
自身属性是直接在对象上定义的。 而原型属性在 prototype
上定义。
function Dog(name) {
this.name = name;
}
Dog.prototype.numLegs = 4;
let beagle = new Dog("Snoopy");
let ownProps = [];
let prototypeProps = [];
for (let property in beagle) {
if (beagle.hasOwnProperty(property)) {
ownProps.push(property); // let in 只能遍历自身属性
} else {
prototypeProps.push(property);
}
}
构造函数属性 Constructor Property
在上一个挑战中创建的实例对象 duck
和 beagle
都有一个特殊的 constructor
属性:
let duck = new Bird();
let beagle = new Dog();
console.log(duck.constructor === Bird); // true
console.log(beagle.constructor === Dog); // true
需要注意到的是: constructor
属性是对创建实例的构造函数的一个引用。 constructor
属性的一个好处是可以通过检查这个属性来找出它是一个什么对象。
注意: 由于 constructor
属性可以被重写,所以最好使用instanceof
方法来检查对象的类型。
给对象添加属性
一种更有效的方法就是给对象的 prototype
设置为一个已经包含了属性的新对象。 这样一来,所有属性都可以一次性添加进来:
Bird.prototype = {
numLegs: 2,
eat: function() {
console.log("nom nom nom");
},
describe: function() {
console.log("My name is " + this.name);
}
};
更改原型时,记得设置构造函数属性
手动设置一个新对象的原型有一个重要的副作用。 它清除了 constructor
属性! 此属性可以用来检查是哪个构造函数创建了实例,但由于该属性已被覆盖,它现在给出了错误的结果:
duck.constructor === Bird; // false
duck.constructor === Object; // true
duck instanceof Bird; // true
为了解决这个问题,凡是手动给新对象重新设置过原型对象的,都别忘记在原型对象中定义一个 constructor
属性:
Bird.prototype = {
constructor: Bird,
numLegs: 2,
eat: function() {
console.log("nom nom nom");
},
describe: function() {
console.log("My name is " + this.name);
}
};
原型 Prototype
在 JS 中,原型是一种属性可以被多个其他对象共享的对象。
对象的原型
来自同一原型的对象有相同的功能。
对象也可直接从创建它的构造函数那里继承其 prototype
。 请看下面的例子:Bird
构造函数创建了一个 duck
对象:
function Bird(name) {
this.name = name;
}
let duck = new Bird("Donald");
duck
从 Bird
构造函数那里继承了它的 prototype
。 你可以使用 isPrototypeOf
方法来验证他们之间的关系:
Bird.prototype.isPrototypeOf(duck); // true
原型链 Prototype Chain
JavaScript 中所有的对象(除了少数例外)都有自己的 prototype
。 而且,对象的 prototype
本身也是一个对象。
function Bird(name) {
this.name = name;
}
typeof Bird.prototype; // object
正因为 prototype
是一个对象,所以 prototype
对象也有它自己的 prototype
! 这样看来的话,Bird.prototype
的 prototype
就是 Object.prototype
:
Object.prototype.isPrototypeOf(Bird.prototype);
hasOwnProperty
是定义在 Object.prototype
上的一个方法,尽管在 Bird.prototype
和 duck
上并没有定义该方法,但是依然可以在这两个对象上访问到。 这就是 prototype
链的一个例子。 在这个prototype
链中,Bird
是 duck
的 supertype
,而 duck
是 subtype
。 Object
则是 Bird
和 duck
实例共同的 supertype
。 Object
是 JavaScript 中所有对象的 supertype
,也就是原型链的最顶层。 因此,所有对象都可以访问 hasOwnProperty
方法。
继承 Inherit
根据以上所说的 DRY 原则,通过创建一个 Animal
supertype
(或者父类)来重写这段代码:
function Animal() { };
Animal.prototype = {
constructor: Animal,
describe: function() {
console.log("My name is " + this.name);
}
};
Animal
构造函数中定义了 describe
方法,可将 Bird
和 Dog
这两个构造函数的方法删除掉:
Bird.prototype = {
constructor: Bird
};
Dog.prototype = {
constructor: Dog
};
继承使用 extends
关键字,例如 class B extends class A
class Animal { /* code */ }
class Bird extends Animal { /* code */ }
class Eagle extends Bird { /* code */ }
从超类继承行为 Inherit Behaviors from a Supertype
这里用到构造函数的继承特性。 首先创建一个超类 supertype
(或者叫父类)的实例。
let animal = new Animal();
此语法用于继承时会存在一些缺点。相反,另外一种没有这些缺点的方法来替代 new 操作:
let animal = Object.create(Animal.prototype);
Object.create(obj)
创建了一个新对象,并指定了 obj
作为新对象的 prototype
。 回忆一下,之前说过 prototype
就像是创建对象的“配方”。 如果把 animal
的 prototype
设置为与 Animal
构造函数的 prototype
一样,那么就相当于让 animal
这个实例具有与 Animal
的其他实例相同的“配方”了。
第二个步骤:给子类型(或者子类)设置 prototype
。 这样一来,Bird
就是 Animal
的一个实例了。
Bird.prototype = Object.create(Animal.prototype);
请记住,prototype
类似于创建对象的“配方”。 从某种意义上来说,Bird
对象的配方包含了 Animal
的所有关键“成分”。
let duck = new Bird("Donald");
duck.eat();
duck
继承了Animal
的所有属性,其中包括了 eat
方法。
super
关键字可以借用父类中的功能。
class Animal {
constructor(color = 'yellow', energy = 100) {
this.color = color;
this.energy = energy;
}
isActive() {
if(this.energy > 0) {
this.energy -= 20;
console.log('Energy is decreasing, currently at:', this.energy)
} else if(this.energy == 0){
this.sleep();
}
}
sleep() {
this.energy += 20;
console.log('Energy is increasing, currently at:', this.energy)
}
getColor() {
console.log(this.color)
}
}
class Cat extends Animal {
constructor(sound = 'purr', canJumpHigh = true, canClimbTrees = true, color, energy) {
super(color, energy);
this.sound = sound;
this.canClimbTrees = canClimbTrees;
this.canJumpHigh = canJumpHigh;
}
makeSound() {
console.log(this.sound);
}
}
class Bird extends Animal {
constructor(sound = 'chirp', canFly = true, color, energy) {
super(color, energy);
this.sound = sound;
this.canFly = canFly;
}
makeSound() {
console.log(this.sound);
}
注意⚠️:如果在子类中不使用 super
关键字,一旦运行上面的程序,会得到 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor.
错误。
重置构造属性 Reset
当一个对象从另一个对象那里继承了其 prototype
时,那它也继承了父类的 constructor 属性。
请看下面的举例:
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor // Bird
但是 duck
和其他所有 Bird
的实例都应该表明它们是由 Bird
创建的,而不是由 Animal
创建的。 为此,你可以手动将 Bird
的构造函数属性设置为 Bird
对象:
Bird.prototype.constructor = Bird;
duck.constructor // Bird
继承后添加方法
从超类构造函数继承其 prototype
对象的构造函数,除了继承的方法外,还可以拥有自己的方法。
除了从 Animal
构造函数继承的行为之外,还需要给 Bird
对象添加它独有的行为。 这里,给 Bird
对象添加一个 fly()
函数。 函数会以一种与其他构造函数相同的方式添加到 Bird's
的 prototype
中:
Bird.prototype.fly = function() {
console.log("I'm flying!");
};
现在 Bird
的实例中就有了 eat()
和 fly()
这两个方法:
let duck = new Bird();
duck.eat();
duck.fly();
duck.eat()
将在控制台中显示字符串 nom nom nom
, duck.fly()
将显示字符串 I'm flying!
。
重写继承的方法 Override
通过使用一个与需要重写的方法相同的方法名,向ChildObject.prototype
中添加方法。 请看下面的举例:Bird
重写了从 Animal
继承来的 eat()
方法:
function Animal() { }
Animal.prototype.eat = function() {
return "nom nom nom";
};
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.eat = function() {
return "peck peck peck";
};
如果一个实例:let duck = new Bird();
,然后调用了 duck.eat()
,以下就是 JavaScript 在 duck
的 prototype
链上寻找方法的过程:
duck
=>eat()
是定义在这里吗? 不是。Bird
=>eat()
是定义在这里吗? => 是的。 执行它并停止往上搜索。Animal
=> 这里也定义了eat()
方法,但是 JavaScript 在到达这层原型链之前已停止了搜索。- Object => JavaScript 在到达这层原型链之前也已经停止了搜索
多态 Polymorphism
多态来源于希腊语,意思是“多种形态”。
例如门和自行车上都有铃,但是它们的使用目的,形态都可能不同。
function ringTheBell(thing) {
console.log(thing.bell())
}
ringTheBell(door);
ringTheBell(bicycle);
例如 concat()
方法和 +
都可以起到字符串拼接的作用,但是它们的形态不同。
多态性允许开发人员构建具有完全相同功能的对象,即具有完全相同名称、行为完全相同的函数。但同时,开发者可以在 OOP 结构的其他部分覆盖共享功能的某些部分,甚至覆盖完整功能。
举例:
class Bird {
useWings() {
console.log('flying');
}
}
class Eagle {
useWings() {
super.useWings();
console.log('barely flying');
}
}
class Penguin extends Bird {
useWings() {
console.log('diving');
}
}
var baldEagle = new Eagle();
var kingPenguin = new Penguin();
baldEagle.useWings(); // flying barely flying
kingPenguin.useWings() // diving
使用 Mixin 在不相关对象之间添加共同行为
对于不相关的对象,更好的方法是使用 mixins。 mixin 允许其他对象使用函数集合。
let bird = {
name: "Donald",
numLegs: 2
};
let boat = {
name: "Warrior",
type: "race-boat"
};
let glideMixin = function(obj) {
obj.glide = function() {
console.log("glide");
}
};
glideMixin(bird);
glideMixin(boat);
私有化 Private
使属性私有化最简单的方法就是在构造函数中创建变量。 可以将该变量范围限定在构造函数中,而不是全局可用。 这样,属性只能由构造函数中的方法访问和更改。
在 JavaScript 中,函数总是可以访问创建它的上下文。 这就叫做 closure
(闭包)。
function Bird() {
let hatchedEgg = 10;
this.getHatchedEggCount = function() {
return hatchedEgg;
};
}
let ducky = new Bird();
ducky.getHatchedEggCount();
立即调用函数表达式(IIFE)
JavaScript 中的一个常见模式就是,函数在声明后立刻执行:
(function () {
console.log("Chirp, chirp!");
})();
这是一个匿名函数表达式,立即执行并输出 Chirp, chirp!
。
请注意,函数没有名称,也不存储在变量中。 函数表达式末尾的两个括号()会让它被立即执行或调用。 这种模式被叫做立即调用函数表达式(immediately invoked function expression) 或者IIFE。
一个立即调用函数表达式(IIFE)通常用于将相关功能分组到单个对象或者是 module 中。 例如,先前的挑战中定义了两个 mixins:
function glideMixin(obj) {
obj.glide = function() {
console.log("Gliding on the water");
};
}
function flyMixin(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
};
}
可以将这些 mixins 分成以下模块:
let motionModule = (function () {
return {
glideMixin: function(obj) {
obj.glide = function() {
console.log("Gliding on the water");
};
},
flyMixin: function(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
};
}
}
})();
注意:一个立即调用函数表达式(IIFE)返回了一个 motionModule
对象。 返回的这个对象包含了作为对象属性的所有 mixin 行为。 module 模式的优点是,所有的运动相关的行为都可以打包成一个对象,然后由代码的其他部分使用。 下面是一个使用它的例子:
motionModule.glideMixin(duck);
duck.glide();