主讲:麓一
面向过程(POP)和面向对象(OOP)
什么是面向对象编程
面向对象是一种编程思想,经常被拿来和面向过程比较。
其实说的简单点:
- 面向过程 关注的重点是动词,是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。
- 例如:获取商品信息 => 下单 => 调起支付
- 面向对象 关注的重点是主谓,是把构成问题的事物拆解为各个对象,而拆解出对象的目的也不是为了实现某个步骤,而是为了描述这个事物在当前问题中的各种行为。
面向对象的特点是什么?
- 封装:让使用对象的人不考虑内部实现,只考虑功能使用,把内部的代码保护起来,只留出一些
api接口供用户使用; - 继承:就是为了代码的复用,从父类上继承出一些方法和属性,子类也有自己的一些属性;
- 多态:是不同对象作用于同一操作产生不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开;
比如下棋的过程:
面向过程是这样的:开局 => 白方下棋 => 棋盘展示 => 检查胜负 => 黑方下棋 => 棋盘展示 => 检查 => 胜负 => 循环
用代码表示可能是一连串函数的调用:
init();whitePlay();// 里面实现一遍下棋的操作repaint();// 棋盘展示check();blackPlay();// 再单独实现一遍下棋的操作repaint();// 棋盘展示check();
面向对象是这样的:棋盘.开局 => 选手.下棋 => 棋盘.重新展示 => 棋盘.检查胜负 => 选手.下棋 => 棋盘.重新展示 => 棋盘.检查胜负
用代码表示可能是这样的:
const checkerBoard = new CheckerBoard();//CheckerBoard类内部封账了棋盘的操作,比如初始化棋盘,检查胜负关系等const whitePlayer = new Player(‘white’);//Player类内部封装了各种玩家的操作,比如等待,落棋,悔棋const blackPlayer = new Player(‘black’);whitePlayer.start();//start方法的结束,内部封装了或者通过事件发布触发checkerBoard.repaint(), checkerBoard.check()的调用blackPlayer.start();
你只需要调用 new 一个 player,然后调用 start 方法,也就是说我们只需要关注行为,而不需要知道内部到底做了什么。
而且如果要加一些新功能,比如悔棋,比如再加一个玩家,面向对象都很好扩展。
在上面的例子中,面向对象的特性是怎么表现出来的呢?
- 封装:
Player,CheckerBoard类,使用的时候并不需要知道内部实现了什么,只需要考虑暴露出的api的使用; - 继承:
whitePlayer和blackPlayer都继承自Player,都可以直接使用Player的各种方法和属性; - 多态:
whitePlayer.start()和blackPlayer.start()下棋的颜色分别是白色和黑色;
什么时候适合使用面向对象
- 面向过程:简单的场景下,协同人员较少;
- 面向对象:中型或者大型项目中,协同人员较多,迭代频繁;
// 面向对象例子
class Food {
cooked() {
if (this.type) {
console.log(`make the ${this.type} can be eat`);
} else {
console.error('should ensure food');
}
}
eat() {
console.log('eating food');
}
}
class Pork extends Food {
constructor() {
super();
this.type = 'pork';
}
}
class Vegetable extends Food {}
class Person {}
class Chief extends Person {
cook(meat) {
meat.cooked();
}
}
class Consumer extends Person {
eat(food) {
// todo
food.eat();
}
}
const chief = new Chief();
const consumer = new Consumer();
const recookMeat = new Pork();
chief.cook(recookMeat);
consumer.eat(recookMeat);
// 类和对象
class Car extends Object {
// speed, monkey, 排量
// drive, bug sell
}
class MockCar extends Object {}
// 动物。为什么动物,
let myCar = new Car();
JS 对象的创建
创建一个对象有哪些方法?
Object.create();var obj = {};new Object(); / new Function();
1、Object.create()
Object.create 创建了一个对象;
let p = Object.create(q); -> p.__proto__ = q; 就是 p 的原型指向了 q;
当我们需要调用 p 对象的一个方法或者属性的时候,如果 p 上面没有,就会去 q 上找;
let q = {};
let p = Object.create(q);
console.log(p.__proto__ === q); // true
console.log(p.__proto__.__proto__ === Object.prototype); // true
2、var obj = {}
obj.__proto__ = Object.prototype;
和 Object.create() 对比:
let p = Object.create({}); 相当于:let p = Object.create(obj); 相当于:p.__proto__ = obj;
所以:p.__proto__.__proto__ = Object.prototype;
var obj = {};
// 相当于:obj.__proto__ = Object.prototype;
console.log(obj.__proto__ === Object.prototype); // true
let p = Object.create({});
// 相当于:
let obj1 = {};
let p1 = Object.create(obj1);
// 相当于:p1.__proto__ = obj1;
console.log(p1.__proto__.__proto__ === Object.prototype); // true
3、new Object(); / new Function();
- 创建了一个对象;
- 该对象的原型,指向了这个
Function(构造函数)的prototype; - 该对象实现了这个构造函数的方法;
- 根据一些特定情况返回对象;
- 如果没有返回值,则返回创建的对象;
- 如果有返回值,是一个对象(除了
null),则返回该对象; - 如果有返回值,不是一个对象,则返回创建的对象;
Object.create的参数,如何和{}的结果保持一致?
- 使用
Object.create(Object.prototype)相当于 直接定义一个{}对象;
Object.create(null)有何问题?
- 没有
Object的原型方法;
new 的实现
function Person(name) {
this.name = name;
return {};
}
function newObj(Father) {
if (typeof Father !== 'function') {
throw new Error('new operator function the first param must be a function!');
}
var obj = Object.create(Father.prototype);
var result = Father.apply(obj, Array.prototype.slice.call(arguments, 1));
return result && typeof result === 'object' && typeof result !== null ? result : obj;
}
// 使用
newObj(Person, 'tom');
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person();
// Person 是构造函数
p.__proto__ = Person.prototype; // = { ..., constructor: Person };
Person.prototype.constructor = Person;
// p 本身没有 constructor,p.constructor 找到的是它的原型,原型上有 Person,所以 p.constructor => Person
p.constructor = Person;
// console.log(p.__proto__ === Person.prototype); // true
// console.log(Person.prototype.constructor === Person); // true
// console.log(p.constructor === Person); // true
继承
实现一个继承,主要就是两部分:
- 使用父类构造函数的方法;
- 让对象的原型链指向父类;
原型链继承
function Parent() {
this.name = 'father';
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child() {}
// Child.prototype.__proto__ = Parent.prototype;
Child.prototype = new Parent();
Child.prototype.constructor = Child;
存在的问题:
- 如果有属性是引用类型,一旦某个实例修改了这个属性,所有的都会被改;
- 创建
Child的时候不能传参;
构造函数继承
想办法把 Parent 上的属性和方法,添加到 Child 上面去,而不是都存在在原型对象上,防止被实例共享;
function Parent(actions, name) {
this.actions = actions || ['eat', 'work', 'sleep'];
this.name = name || 'parentName';
}
function Child(play) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.play = play;
}
存在的问题:
- 属性或者方法被继承的话,只能在构造函数中定义;
- 如果方法在构造函数中定义了,那么每次创建实例都会创建一遍方法;
组合继承
function Parent(actions, name) {
this.actions = actions || ['eat', 'work', 'sleep'];
this.name = name || 'parentName';
}
Parent.prototype.work = function () {
console.log(`${this.name} coding everyday.`);
};
function Child(play) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.play = play;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
存在的问题:
- 调用了两次构造函数,做了重复的操作;
- 第一次是在构造函数继承的时候;
- 第二次是在原型链继承的时候;
寄生组合式继承
// 方法1
function Parent(actions, name) {
this.actions = actions || ['eat', 'work', 'sleep'];
this.name = name || 'parentName';
}
Parent.prototype.work = function () {
console.log(`${this.name} coding everyday.`);
};
function Child(play) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.play = play;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 方法2
function inherit(p) {
// p 是一个对象,但不能是 null
if (p === null) throw TypeError();
// 如果 Object.create 存在,直接使用
if (Object.create) {
return Object.create(p);
}
var t = typeof p;
if (t !== 'object' && t !== 'function') throw TypeError();
// 定义一个空构造函数
function f() {}
// 将其原型属性设置为 p
f.prototype = p;
// 使用 f() 创建 p 的继承对象
return new f();
}
function Parent(actions, name) {
this.actions = actions || ['eat', 'work', 'sleep'];
this.name = name || 'parentName';
}
Parent.prototype.work = function () {
console.log(`${this.name} coding everyday.`);
};
function Child(play) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.play = play;
}
// Child.prototype = Object.create(Parent.prototype);
Child.prototype = inherit(Parent.prototype);
Child.prototype.constructor = Child;
ES6 class 继承
class Parent {}
class Child extends Parent {
constructor() {
super();
// this.xxx = xxx;
}
}
super()作为函数调用时,要求子类必须执行一次;- 因为子类自己的
this对象,必须通过父类的构造函数完成; es6继承会继承静态的属性和方法;
补充
一、JS 中怎么创建对象
1、普通方式/工厂模式
- 缺点:每一个新对象都要重新写一遍
color和start的赋值;
const Player = new Object();
Player.color = 'white';
Player.start = function () {
console.log('white 下棋');
};
console.log(Player); // { color: 'white', start: [Function (anonymous)] }
Player.start(); // white 下棋
console.log(Player.constructor); // [Function: Object],不能判断类型
或者工厂模式,这两种方式都无法识别对象类型,比如 Player 的类型只是 Object。
function createObject(color) {
const Player = new Object();
Player.color = color;
Player.start = function () {
console.log('下棋');
};
return Player;
}
const red = createObject('red');
console.log(red);
red.start(); // 下棋
console.log(red.constructor); // [Function: Object]
缺点:无法判断类型。
2、构造函数/实例
通过 this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。
但是这样创建的好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)。
function Player() {
this.color = 'red';
this.start = function () {
console.log(this.color);
};
}
const p1 = new Player();
const p2 = new Player();
console.log(p1); // Player {color: "red", start: ƒ}
console.log(p1.constructor); // [Function: Player]
p1.start(); // red
console.log(p1.start === p2.start); // false,说明每生成一个实例,构造函数内部的方法都会重新开辟一块内存
优点:相比于普通模式和工厂模式,可以知道类型。
缺点:每生成一个实例,构造函数内部的方法都会重新开辟一块内存。
3、原型
通过原型继承的方法并不是自身的,我们要在原型链上一层一层的查找,这样创建的 好处是只在内存中创建一次,实例化的对象都会指向这个 prototype 对象。
function Player() {
this.color = 'red';
}
Player.prototype.start = function () {
console.log(this.color);
};
const p1 = new Player();
const p2 = new Player();
console.log(p1.start === p2.start); // true
console.log(p1); // Player { color: 'red' }
console.log(p1.constructor); // [Function: Player]
优点:是只在内存中创建一次,实例化的对象都会指向这个 prototype 对象。
缺点:共享同一个原型。
4、静态属性
静态属性就是绑定在构造函数上的属性方法,需要通过构造函数访问。
比如我们想看一下一共创建了多少个玩家的实例:
function Player() {
this.color = 'red';
// total 就是静态属性
if (!Player.total) {
Player.total = 0;
}
Player.total++;
}
const p1 = new Player();
console.log(Player.total); // 1
const p2 = new Player();
console.log(Player.total); // 2
二、原型及原型链
1、在原型上添加属性或者方法有什么好处?
在构造函数内通过 this 添加方法的话,每生成一个对象,都会重新开辟一块内存空间,当对象变多之后,性能会变得很差:
function Player() {
this.color = 'red';
this.start = function () {
console.log(this.color);
};
}
但是通过在原型上添加只在内存中创建一次:
Player.prototype.xxx = function () {};
Player.prototype.xxx = function () {};
Player.prototype.xxx = function () {};
这种方式向原型对象添加属性或者方法的话,又显得非常麻烦,所以我们可以这样写:
Player.prototype = {
start: function () {
console.log('下棋');
},
revert: function () {
console.log('悔棋');
}
};
2、怎么找到 Player 的原型对象?
-
xxx.__proto__ -
Object.getPrototypeOf(xxx)
function Player(color) {
this.color = color;
}
Player.prototype.start = function () {
console.log(color + '下棋');
};
const p1 = new Player('white');
const p2 = new Player('black');
// __proto__ 指向实例的原型对象
console.log(p2.__proto__); // Player {}
console.log(Object.getPrototypeOf(p2)); // Player {},可以通过 Object.getPrototypeOf 来获取 __proto__
console.log(Player.prototype); // Player {}
console.log(p2.__proto__ === Player.prototype); // true
console.log(Player.__proto__); // [Function (anonymous)]
console.log(Player.prototype.constructor); // [Function: Player]
console.log(Player.prototype.constructor === Player); // true
可以看一下 prototype.png 原型的流程图:
3、new 关键字到底做了什么?
- 一个继承自
Player.prototype的新对象p1被创建; p1.__proto__指向Player.prototype,即p1.__proto__ = Player.prototype;(继承)- 将
this指向新创建的对象p1; - 返回这个新对象
p1;- 未显式
return,返回新对象p1; - 显式
return this,返回新对象p1; - 显式
return基本数据类型,则this指向保持原来的规则,返回新对象p1; - 显式
retuen对象类型,this指向返回的对象,比如{ a: 1 },则返回这个对象{ a: 1 }; return null,null比较特殊,null的数据类型为对象,但是this指向保持原来的规则;
- 未显式
3.1 new 的过程
new 的过程包括以下四个阶段:
- 创建一个新对象;
let obj = new Object();
- 这个新对象的
__proto__属性指向原函数的prototype属性;(即继承原函数的原型)- 获取原函数:
let FunctionConstructor = [].shift.call(arguments); obj.__proto__ = [FunctionConstructor].prototype;
- 获取原函数:
- 将这个新对象绑定到此函数的
this上;let resultObj = [FunctionConstructor].apply(obj, ...args);let resultObj = [FunctionConstructor].apply(obj, arguments);
- 返回新对象;
return typeoj resultObj === "object" && resultObj !== null ? resultObj : obj;return resultObj instanceof Object ? resultObj : obj;
后面看一下怎么手写实现 new 函数:
3.2 代码实现
/*
1. 用 new Object() 的方式新建了一个对象 obj
2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
5. 返回 obj
*/
/*
new 实现的过程:
1、创建一个新对象
2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
3、将这个新对象绑定到此函数的 this 上;
4、返回这个新对象 p1
*/
function objectFactory() {
// 1、创建一个对象
let obj = new Object();
// arguments 是类数组对象
/*
Javascript 函数中的参数对象 arguments 是个对象,而不是数组。
但它可以类似数组那样通过数字下表访问其中的元素,而且它也有 length 属性标识它的元素的个数。
通常我们把它转换成数组用 Array 的 slice 函数,示例代码如下:
function fn() {
var arr = Array.prototype.slice.call(arguments,0);
}
*/
// 2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
// let FunctionConstructor = Array.prototype.shift.call(arguments); // 返回的就是 Player
// shift 改变原数组
let FunctionConstructor = [].shift.call(arguments); // 返回的就是 Player
console.log(FunctionConstructor); // [Function: Player]
obj.__proto__ = FunctionConstructor.prototype;
// 3、将这个新对象绑定到此函数的 this 上;
let resultObj = FunctionConstructor.apply(obj, arguments);
// return typeof resultObj === "object" && resultObj !== null ? resultObj : obj;
// null instanceof Object => false
// typeof null => Object
// 4、返回这个新对象 p1
return resultObj instanceof Object ? resultObj : obj;
}
function Player(name) {
this.name = name;
// return {
// name: "tom"
// };
// return 123;
// return null;
}
const p1 = objectFactory(Player, '秋裤');
console.log(p1); // Player { name: '秋裤' }
console.log(p1.name); // 秋裤
- 去掉注释
/*
new 实现的过程:
1、创建一个新对象
2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
3、将这个新对象绑定到此函数的 this 上;
4、返回这个新对象 p1
*/
function objectFactory() {
// 1、创建一个对象
let obj = new Object();
// 2、这个新对象的 __proto__ 属性指向原函数的 prototype 属性;(即继承原函数的原型)
let FunctionConstructor = [].shift.call(arguments); // 返回的就是 Player
obj.__proto__ = FunctionConstructor.prototype;
// 3、将这个新对象绑定到此函数的 this 上;
let resultObj = FunctionConstructor.apply(obj, arguments);
// 4、返回这个新对象 p1
return resultObj instanceof Object ? resultObj : obj;
}
function Player(name) {
this.name = name;
// return {
// name: "tom"
// };
// return 123;
// return null;
}
const p1 = objectFactory(Player, '秋裤');
console.log(p1); // Player { name: '秋裤' }
console.log(p1.name); // 秋裤
4、原型链到底是什么?
我们都知道当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
Object.prototype.name = 'Object';
function Player() {}
Player.prototype.name = 'Player';
var p1 = new Player();
p1.name = 'Daisy';
// 查找 p1 对象中的 name 属性,因为上面添加了 name,所以会输出 Daisy
console.log(p1.name); // Daisy
delete p1.name; // 删除自己的 name 属性
// 删除了 p1.name,然后查找 p1 发现没有 name 属性,
// 就会从 p1 的原型 p1.__proto__ 中去找,也就是 Player.prototype,然后找到了 name,输出 Player
console.log(p1.name); // Player
delete Player.prototype.name; // 删除原型上的 name 属性
console.log(p1.name); // Object
这样一条通过 __proto__ 和 prototype 去连接的对象的链条,就是原型链。
三、继承
1、原型链继承
function Parent(name) {
this.name = name || 'Parent';
this.eat = function () {
console.log(111);
};
}
Parent.prototype.actions = ['sing'];
function Child() {}
// 这样写只能继承 Parent 原型上的属性和方法,不能继承实例上的
Child.prototype = Parent.prototype;
const c1 = new Child('我是 c1');
console.log(c1.name); // undefined
console.log(c1.actions); // [ 'sing' ]
c1.eat(); // 此处也找不到 eat 方法,报错:TypeError: c1.eat is not a function
1.1 代码实现
function Parent(name) {
this.name = name || 'Parent';
this.actions = ['sing', 'jump', 'rap'];
}
function Child() {}
Child.prototype = new Parent('我是 Parent');
// 为了保证类型正确,我们需要将 Child.prototype.constructor 指向它原本的构造函数 Child
// 如果不写此行代码,new 出来的实例的 constructor 是指向 [Function: Parent]
Child.prototype.constructor = Child;
const c1 = new Child('我是 c1');
const c2 = new Child();
c1.actions.push('basketball');
console.log(c1.constructor); // [Function: Parent] [Function: Child]
console.log(c1.name); // 我是 Parent
console.log(c1.actions); // ["sing", "jump", "rap", "basketball"]
console.log(c2.actions); // ["sing", "jump", "rap", "basketball"]
1.2 缺点
- 父类如果存在【引用类型】,其中一个实例如果改变了此变量,那么所有的实例都会共享;
- 无法传参给
Parent;
2、构造函数继承
看到上面的问题 1,我们想一下该怎么解决呢?
能不能想办法把 Parent 上的属性方法,添加到 Child 上呢?而不是都存在原型对象上,防止被所有实例共享。
1.1 代码实现
针对问题 1 我们可以使用 call 来复制一遍 Parent 上的操作。
function Parent() {
this.name = 'Parent';
this.actions = ['sing', 'jump', 'rap'];
}
function Child() {
Parent.call(this);
// ==> 相当于拷贝父元素
// this.name = 'Parent';
// this.actions = ['sing', 'jump', 'rap'];
}
const c1 = new Child();
c1.actions.push('basketball');
console.log(c1.constructor); // [Function: Child]
console.log(c1.actions); // [ 'sing', 'jump', 'rap', 'basketball' ]
const c2 = new Child();
console.log(c2.actions); // [ 'sing', 'jump', 'rap' ]
针对问题 2 我们应该怎么传参呢?
function Parent(name, color) {
this.name = name;
this.color = color;
this.actions = ['sing', 'jump', 'rap'];
this.eat = function () {
console.log(`${name} - eat`);
};
}
function Child() {
// arguments 是类数组对象,得用 apply,不能用 call
Parent.apply(this, arguments);
}
const c1 = new Child('c1', 'red');
const c2 = new Child('c2', 'white');
console.log(c1);
console.log(c1.constructor); // [Function: Child]
/*
Child {
name: 'c1',
color: 'red',
actions: [ 'sing', 'jump', 'rap' ],
eat: [Function (anonymous)]
}
*/
console.log(c2);
/*
Child {
name: 'c2',
color: 'white',
actions: [ 'sing', 'jump', 'rap' ],
eat: [Function (anonymous)]
}
*/
console.log(c1.actions === c2.actions); // false
console.log(c1.eat === c2.eat); // false
1.2 缺点
- 浪费内存:如果属性或者方法想被继承,只能在构造函数中定义。而如果方法在构造函数内定义了,那么每次创建实例都会创建一遍方法,多占一块内存;
3、组合继承
通过原型链继承我们实现了基本的继承,方法存在 prototype 上,子类可以直接调用。但是引用类型的属性会被所有实例共享,并且不能传参。
通过构造函数继承,我们解决了上面的两个问题:使用 call/apply 在子构造函数内重复一遍属性和方法创建的操作,并且可以传参了。
但是构造函数继承同样带来了一个问题,就是构造函数内重复创建方法,导致内存占用过多。
是不是突然发现原型链继承是可以解决方法重复创建的问题? 所以我们将这两种方式结合起来,这就叫做组合继承。
1.1 代码实现
/*
组合继承:原型链继承 + 构造函数继承
*/
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function () {
console.log(`${this.name} - eat`);
};
function Child() {
// 构造函数继承
Parent.apply(this, arguments); // 第一次调用构造函数
}
// 原型链继承
Child.prototype = new Parent(); // 第二次调用构造函数
Child.prototype.constructor = Child;
const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);
console.log(c1); // Child { name: 'c1', actions: [ 'eat', 'play' ] }
console.log(c2); // Child { name: 'c2', actions: [ 'run' ] }
c1.eat(); // c1 - eat
c2.eat(); // c2 - eat
console.log(c1.eat === c2.eat); // true
1.2 缺点
- 调用了两次构造函数,做了重复的操作
- 第一次是在构造函数继承的时候;
- 第二次是在原型链继承的时候;
4、寄生组合式继承
上面重复调用了 2 次构造函数,想一下,我们可以精简掉哪一步?
我们可以考虑让 Child.prototype 间接访问到 Parent.prototype。
1.1 代码实现
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function () {
console.log(`${this.name} - eat`);
};
function Child() {
Parent.apply(this, arguments);
}
// Child.prototype = new Parent(); // 优化
// Child.prototype = Parent.prototype; // 不可以这样写,因为:在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变
/*
Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。
返回值:一个新对象,带着指定的原型对象和属性。
*/
Child.prototype = Object.create(Parent.prototype);
/*
! Object.create 的内部实现原理:
let TempFunction = function () {};
TempFunction.prototype = Parent.prototype;
Child.prototype = new TempFunction();
*/
Child.prototype.constructor = Child;
const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);
Child.prototype.run = function () {
console.log('run');
};
console.log('Child ==== ', Child.prototype);
console.log('Parent ===== ', Parent.prototype);
也许有的同学会问,为什么一定要通过桥梁的方式让 Child.prototype 访问到 Parent.prototype?
直接 Child.prototype = Parent.prototype 不行吗?
答:不行!!
咱们可以来看一下:
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function () {
console.log(`${this.name} - eat`);
};
function Child() {
Parent.apply(this, arguments);
}
// Child.prototype = new Parent(); // 优化
Child.prototype = Parent.prototype; // 在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变
Child.prototype.constructor = Child;
const c1 = new Child('c1', ['eat']);
c1.actions.push('play');
const c2 = new Child('c2', ['run']);
Child.prototype.run = function () {
console.log('run');
};
console.log(Child.prototype); // {eat: ƒ, run: ƒ, constructor: ƒ}
console.log(Parent.prototype); // {eat: ƒ, run: ƒ, constructor: ƒ}
可以看到,在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变,这可不是我们想看到的。
5、class 继承
1.1 代码实现
class Parent {
constructor() {
this.name = 'parent';
this.actions = ['sing'];
this.eat = function () {
console.log('this is eat func');
};
}
getName() {
console.log('this is getName func');
}
}
// 继承使用 extends
class Child extends Parent {
constructor(name) {
// console.log(super()); // super() 执行后返回的就是继承自 Parent 的实例
/*
Child {
name: 'parent',
actions: [ 'sing' ],
eat: [Function (anonymous)]
}
*/
super();
this.name = name;
}
}
const c1 = new Child('c1');
const c2 = new Child('c2');
c1.actions.push('rap');
console.log(c1.name); // c1
console.log(c2.name); // c2
console.log(c1.actions); // [ 'sing', 'rap' ]
console.log(c2.actions); // [ 'sing' ]
console.log(c1.eat === c2.eat); // false
console.log(c1.getName === c2.getName); // true