前言
面试官:“你怎么处理继承?”
我:“我不造啊......-_-!!!”
一、认识原型
1.对象的原型
1.1认识[[prototype]]
JavaScript中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象,指向的对象就是对象的原型。通过字面量创建的对象也有这个属性,只要是对象都有这个属性。
1.2获取原型
- 并不存在真实的属性名
[[prototype]],它只是一个标准。所以无法通过常规获取对象属性的方式获取。
- 可以通过以下两种方式获取原型:
- 非标准方法:
obj.__proto__(早期浏览器自己添加的,有一定兼容性问题)。 - 标准方法:
Object.getPrototypeOf(obj)。
obj.__proto__ === Object.getPrototypeOf(obj) // true
如果浏览器不实现__proto__,则无法使用obj.__proto__。所以一般把__proto__叫做隐式原型。
1.3原型的作用
- 当通过引用对象的属性
key来获取value时(如obj.name,obj['name']),会触发[[Get]]的操作。 - 这个操作会先检查该对象是否有对应的属性,如果有就使用它;如果没有就会访问对象
[[prototype]]内置属性指向的对象上的属性。
let obj = { name: 'why' }
console.log(obj.message) // undefined
// 只是举个栗子,实际开发最好别这么做!!!
obj.__proto__.message = 'hello'
console.log(obj.message) // hello
一句话总结:用来查找属性,如果在自己的对象找不到,就去原型上查找。
2.函数的原型
2.1认识prototype
所有的函数都有一个prototype属性。函数原型和对象原型的区别:
-
函数原型
prototype是显式原型,对象原型__proto__是隐式原型。 -
只有函数有
prototype属性,对象没有。显示原型:是可以直接获取的。 隐式原型:一般不会去获取。如果浏览器不实现,则没有这个属性。
2.2获取原型
- 将函数看成普通对象时,它具备
__proto__。 - 将函数看成函数时,它具备
prototype。
prototype是显式原型,所以可以直接通过常规方法获取:foo.prototype。
2.3原型的作用
2.3.1 new操作符
new的过程:
- 创建空对象
var obj = {}
- 将这个空对象赋值给this
this = obj
- 将
函数的显式原型赋值给这个对象作为它的隐式原型
obj.__proto__ = Foo.prototype
- 执行函数体中的代码
- 将这个对象默认返回
// 举个栗子
function Foo () {
}
var foo = new Foo()
// 上面的操作相当于:
// f = {}
// f.__proto__ = Foo.prototype
// 此时
console.log(foo.__proto === Foo.prototype) // true
2.3.2 将函数定义放到原型上
当多个对象拥有共同的值(一般是函数)时,可以将这些值放到构造函数对象的显式原型上,这样由构造函数创建出来的所有对象,都会共享这些属性。
function Student (name, age, sno) {
this.name = name
this.age = age
this.sno = sno
// 方式1:编写函数,会创建很多函数对象
this.running = function () {
console.log(this.name + 'running');
}
this.eating = function () {
console.log(this.name + 'eating');
}
}
// 方式2:
Student.prototype.running = function () {
console.log(this.name + 'running');
}
Student.prototype.eating = function () {
console.log(this.name + 'eating');
}
var stu1 = new Student('why1', 18, 111)
var stu2 = new Student('why2', 19, 112)
var stu3 = new Student('why3', 20, 113)
// 隐式原型的作用:
// 1.stu1的隐式原型是Student.prototype对象
// 2.stu1.running查找:
// 先在自己身上查找,没有找到
// 去原型上查找
stu1.running()
2.3.3 原型的作用
在通过new操作创建对象时,将这个显式原型赋值给创建出来的对象的隐式原型。
一句话总结:用来构建对象时,给对象设置隐式原型。
小声比比:“其实就是要记住这个构造函数,然后把这个构造函数的信息传给构造函数构造出来的对象。最后的最后,还是为了让对象通过原型找属性。”
3.显示原型中的属性
默认情况下,显示原型上都会添加一个属性:constructor,这个属性指向当前的函数对象。
function Person () {
}
var prototype = Person.prototype
console.log(prototype.constructor) // [Function: Person]
console.log(prototype.constructor === Person) // true
4.创建对象过程(内存图预警)
- 定义构造函数
function Person (name, age) {
this.name = name
this.age = age
}
- 首先,在
堆内存中创建一个函数对象,这个函数对象有name、length等属性,还有一个prototype属性指向显示原型对象。显示原型对象上有一个constructor属性,指向函数本身,构成循环引用。
- 使用new创建对象
var p1 = new Person('why', 18)
var p2 = new Person('why1', 19)
new操作符的过程:先创建一个空的对象。- 将
Person的显式原型赋值给新对象的隐式原型。 - 执行代码体里的代码。
- 将这个对象默认返回
- 给函数原型添加方法
Person.prototype.running = function () {
console.log('running');
}
- 创建一个
running函数对象,这个函数对象有name、length等属性。 - 在
Person原型对象上新增一个running属性,并指向running函数对象。
- 获取属性
console.log(p1.name);
console.log(p2.name);
p1.running()
p2.running()
p1实例对象上有name属性,可以直接获取,值为'why'。p2实例对象上有name属性,可以直接获取,值为'why1'。p1对象上没有running属性,顺着隐式原型找到Person显式原型对象,找到running属性,则执行这个running方法。p2对象上没有running属性,顺着隐式原型找到Person显式原型对象,找到running属性,则执行这个running方法。
- 新增属性
Person.prototype.address = '中国'
p1.__ptoto__.info = '卷王'
p1.height = 1.88
p2.isAdmin = true
- 在
Person显式原型对象上添加address属性并赋值。 p1的__ptoto__指向Person显式原型对象,所以info属性被添加到Person显式原型对象上,并赋值。- 给
p1实例对象添加height属性并赋值。 - 给
p2实例对象添加isAdmin属性并赋值。
- 新增属性后获取属性
console.log(p1.address);
console.log(p2.isAdmin);
console.log(p1.isAdmin);
console.log(p2.info);
p1实例对象上没有address属性,顺着隐式原型找到Person显式原型对象,找到address属性,获取值为中国。p2实例对象上有isAdmin属性,可以直接获取,值为true。p1实例对象上没有isAdmin属性,顺着隐式原型找到Person显式原型对象,Person显式原型对象上也没有isAdmin属性,则值为undefined。p2实例对象上没有info属性,顺着隐式原型找到Person显式原型对象,找到info属性,获取值为卷王。
- 修改属性的值并获取
p1.address = '地球'
console.log(p2.address);
- 给
p1实例对象添加height属性并赋值。 p2实例对象上没有address属性,顺着隐式原型找到Person显式原型对象,找到address属性,获取值为中国。
// 完整代码
// 1.定义构造函数
function Person (name, age) {
this.name = name
this.age = age
}
// 3.给函数原型添加方法
Person.prototype.running = function () {
console.log('running');
}
// 2.使用new创建对象
var p1 = new Person('why', 18)
var p2 = new Person('why1', 19)
// 4.获取属性
console.log(p1.name);
console.log(p2.name);
p1.running()
p2.running()
// 5.新增属性
Person.prototype.address = '中国'
p1.__ptoto__.info = '卷王'
p1.height = 1.88
p2.isAdmin = true
// 6.新增属性后获取属性
console.log(p1.address);
console.log(p2.isAdmin);
console.log(p1.isAdmin);
console.log(p2.info);
// 7.修改属性的值并获取
p1.address = '地球'
console.log(p2.address);
5.重写原型对象
function Person () {}
- 在原有的原型对象上添加新的属性
Person.prototype.message = 'message'
Person.prototype.info = '卷王'
Person.prototype.running = function () {}
Person.prototype.eating = function () {}
- 在原型对象上不断的添加新属性
- 直接赋值一个新的原型对象
Person.prototype = {
message: 'message',
info: '卷王',
running: function () {},
eating: function () {}
}
Object.defineProperty(Person.prototype, 'constructor', {
configurable: false,
writable: false,
enumerable: false,
value: Person
})
- 在内存中创建一个新的原型对象,将函数对象的原型指向这个新的原型对象。
缺点:
- 需要手动添加
constructor属性,并指向构造函数。 - 同时
constructor属性需要不可枚举。
注:一般不会重写原型对象。如果要重写,细节也要符合标准。
二、ES5中的继承
1.认识面向对象
面向对象是一种编程范式,正如函数式编程。函数式编程就是就将代码抽象成函数,让函数作为一等公民通过传递调用的方式使用。面向对象编程就是把代码抽象成对象使用。
面向对象的三大特性:
封装:将属性和方法封装到一个类中。继承:是面向对象非常重要的特性。子类继承父类的代码,不仅可以减少代码重复的数量,也是多态的前提(纯面向对象中)。多态:不同的对象在执行时表现出不同的形态。
2.认识继承
继承可以将重复的代码和逻辑抽取到父类中,子类继承过来使用即可。在很多编程语言中,继承也是多态的前提。
function Student (name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
}
Student.prototype.running = function () {}
Student.prototype.eating = function () {}
Student.prototype.studying = function () {}
function Teacher (name, age, title) {
this.name = name
this.age = age
this.title = title
}
Teacher.prototype.running = function () {}
Teacher.prototype.eating = function () {}
Teacher.prototype.teaching = function () {}
Student类和Teacher类有共同的name、age属性和running、eating方法,此时可以将这些共同的内容放到一个Person类中,然后再继承Person类。
一句话总结:继承的目的就是重复利用另一个对象的属性和方法。
3.认识原型链
从一个对象上获取属性,如果在当前对象中没有获取到,就会去它的原型上面获取,如果原型对象上没有,就去原型对象的原型继续查找。原型对象和原型对象之间形成的链条就是原型链。
var obj = {
name: 'why',
age: 18
}
// 等价于
// var obj = new Object()
console.log(obj.__proto__);
console.log(obj.__proto__.__proto__); // null
- 原型链不会永无止境的查找,
obj.__proto__找到Object的原型对象,Object原型对象上也有__proto__,此时obj.__proto__.__proto__指向null,就是顶层原型。当查找到顶层原型就不再继续查找。
注:Object类是所有类的父类。构造函数原型对象也是一个对象,它的隐式原型([[ptotoytpe]])对象是null,因为Object函数原型已经是顶层原型。
console.log(Object.prototype.__proto__) // null
4.原型链实现方法的继承
继承内容包含:属性和方法。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running');
}
Person.prototype.eating = function () {
console.log('eating');
}
function Student (name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
}
Student.prototype.running = function () {
console.log('running');
}
Student.prototype.eating = function () {
console.log('eating');
}
Student.prototype.studying = function () {
console.log('studying');
}
4.1方案1(不可用)
父类的原型直接赋值给子类的原型。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running');
}
Person.prototype.eating = function () {
console.log('eating');
}
function Student (name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
}
// 方案1:Student原型直接指向Person原型
Student.prototype = Person.prototype
// Student.prototype.running = function () {
// console.log('running');
// }
// Student.prototype.eating = function () {
// console.log('eating');
// }
Student.prototype.studying = function () {
console.log('studying');
}
// 创建学生
var stu1 = new Student('why', 18, 111, 100)
stu1.running()
Student原型直接指向Person原型。
缺点:只继承了方法,没实现属性继承。而且父类和子类共享一个原型对象,修改了一个,另一个也会被修改。
4.2方案2(可用但不完美)
创建一个父类的实例对象(new Person()),用这个实例对象作为子类的原型对象。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running');
}
Person.prototype.eating = function () {
console.log('eating');
}
function Student (name, age, sno, score) {
// this.name = name
// this.age = age
this.sno = sno
this.score = score
}
var p = new Person('whyp', 20)
Student.prototype = p
// Student.prototype.running = function () {
// console.log('running');
// }
// Student.prototype.eating = function () {
// console.log('eating');
// }
Student.prototype.studying = function () {
console.log('studying');
}
// 创建学生
var stu1 = new Student('why', 18, 111, 100)
stu1.running()
基本实现继承:
- 定义构造函数。
- 父类原型添加内容。
- 定义子类构造函数。
- 创建
父类对象,并且作为子类原型对象。 - 在子类原型上添加内容。
缺点:属性继承了又没继承,某些属性是保存在p对象上的。
- 直接打印对象看不到这个属性。
- 这个属性会被多个对象共享,如果这个对象是一个引用类型,会造成一些问题。
- 不能给
Person传递参数,让每个stu都有自己的属性,无法定制化。
5.借用构造函数实现属性的继承
为了解决原型链继承中存在的问题,出现新的技术:constructor stealing(借用构造函数/经典继承/伪造对象)。
原理:在子类构造函数的内部调用父类构造函数。
- 因为函数可以在
任意时刻被调用。 - 可以通过
apply()和call()可以在新创建的对象上执行构造函数。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function () {
console.log('running');
}
Person.prototype.eating = function () {
console.log('eating');
}
function Student (name, age, sno, score) {
// this.name = name
// this.age = age
// 在`子类构造函数`的内部调用`父类构造函数`
Person.call(this, name, age)
this.sno = sno
this.score = score
}
Student.prototype.running = function () {
console.log('running');
}
Student.prototype.eating = function () {
console.log('eating');
}
Student.prototype.studying = function () {
console.log('studying');
}
// 创建学生
var stu1 = new Student('why', 18, 111, 100)
stu1.running()
至此,原型链方法继承+借用属性继承基本上实现了ES5的继承。组合借用继承是ES5常用的继承模式之一。
缺点:
- 无论在什么情况下,都会
调用两次父类构造函数。- 创建子类原型(
function Student () {})。 - 创建子类实例(
new Student())。
- 创建子类原型(
- 所有子类实例属性都会
拥有两份父类属性。
6.寄生组合式继承-最终继承方案
思路:就是优化p对象。
观察发现需要满足的条件:
- 必须创建一个对象。
- 这个
对象的隐式原型必须指向父类的显示原型。 - 将这个对象赋值给
子类的显示原型。
- 思路
function Person () {}
function Student () {}
var obj = {}
obj.__proto__ = Person.prototype
Student.prototype = obj
- 实现(重点)
// 封装成工具函数,最终目的都是使stu对象的原型指向Person对象
// 1.创建对象
// 方案1
function createObject (obj) {
function F() {}
F.prototype = obj
return new F()
}
// 方案2
// function createObject (obj) {
// var newObj = {}
// Object.setPrototypeOf(newObj, obj)
// return newObj
// }
// 方案3:使用Object.create创建子类对象
// var stu = Object.create(Person, {
// info: {
// value: '卷王',
// enumerable: true
// }
// })
// 2.将subtype和supertype联系在一起
function inherit (Subtype, Supertype) {
// 方案1
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, 'construtor', {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
// 方案2:有兼容性问题
// Subtype.prototype.__proto__ = Supertype.prototype
// 方案3:有兼容性问题
// Object.setPrototypeOf(Subtype.prototype, Supertype.prototype)
}
- 使用
function Person () {}
function Student () {}
inherit(Student, Person)
以上方案使用到了原型链、借用、原型式(对象之间)、寄生式函数等知识点,所以称之为寄生组合式继承。
三、总结
遇事不决,先画个内存图,会发现所有的方案就是对象间的组合和相互引用。每个最终方案都集成了前辈的思想,在不断试错中达成一致。而世界又在不断的进步,当出现新的需求,方案又要调整,出现各种新的api。所以入了这行,除了卷,没别的办法。