原型以及ES5中实现继承
- 1. 对象和函数的原型
- 1. 普通对象的原型 [[prototype]]
- 2. 函数的原型 prototype
- 2. new、constructor
- 1. new 操作符
- 2. constructor属性
- 3. 将方法放到原型上
- 4. 创建对象的内存表现
- 5. 重写原型对象
- 3. 原型链的查找顺序
- 4. 原型链实现的继承
- 5. 借用构造函数继承
- 6. 寄生组合实现继承
1. 对象和函数的原型
1. 普通对象的原型 [[prototype]]
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象
当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作,这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它,如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性
获取原型的方式
通过对象的 __proto__
属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)
通过 Object.getPrototypeOf 方法可以获取到
var obj = {
name: "why",
age: 18
}
console.log(obj)
var info = {}
// 获取对象的原型
console.log(obj.name, obj.age) // 在这里我们可以直接获取对象自身的属性
console.log(obj.__proto__) // 不推荐使用
console.log(Object.getPrototypeOf(obj)) // 标准使用
console.log(obj.__proto__ === Object.getPrototypeOf(obj)) // true 因为他们指向的是同一个对象
// 疑问: 这个对象的原型有什么用呢?
// 当我们通过[[get]]方式获取一个属性对应的value时
// 1> 它会优先在自己的对象中查找, 如果找到直接返回
// 2> 如果没有找到, 那么会在原型对象中查找
obj.__proto__.message = "Hello World"
console.log(obj.message) // Hello World
但是不管是通过字面量直接创建一个对象还是使用构造函数,只要是对象都会有这样的一个内置属性
2. 函数的原型 prototype
所有的函数都有一个prototype的属性(注意:不是 __proto__
),但是对象是没有prototype属性的
因为它是一个函数,才有了这个特殊的属性,而不是它是一个对象,所以有这个特殊的属性
var obj = {}
function foo() {}
// 1.将函数看成是一个普通的对象时, 它是具备__proto__(隐式原型)
console.log(obj.__proto__) // 获取的是对象的原型
console.log(foo.__proto__) // ƒ () { [native code] }
// foo这个函数本身是Function的一个实例对象
console.log(foo.__proto__ === Function.prototype); //true
// Function是由其本身自己构造出来的。所有函数都是大写Function的实例
console.log(Function.__proto__ === Function.prototype); //true
// 2.将函数看成是一个函数时, 它是具备prototype(显式原型)
// 作用: 用来构建对象时, 给对象设置隐式原型的
console.log(foo.prototype)
// console.log(obj.prototype) 需要注意的是 对象是没有prototype 的!!!
2. new、constructor
1. new 操作符
new 关键字的步骤
- 在内存中创建一个新的对象(空对象)
- 将空对象赋值给 this
- 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
- 执行函数体的代码
- 将这个对象默认返回
function Foo() {
// 1.创建空的对象
// 2.将Foo的prototype原型(显式隐式)赋值给空的对象的__proto__(隐式原型)
}
console.log(Foo.prototype)
var f1 = new Foo()
var f2 = new Foo()
var f3 = new Foo()
var f4 = new Foo()
var f5 = new Foo()
console.log(f1.__proto__)
console.log(f1.__proto__ === Foo.prototype) // true
console.log(f3.__proto__ === f5.__proto__) // true
那么也就意味着我们通过构造函数创建出来的所有对象的[[prototype]]属性都指向Foo.prototype
2. constructor属性
原型对象上面是有一个constructor属性的, 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象
// 非常重要的属性: constructor, 指向Person函数对象
function Person() {
}
// 1.对constructor在prototype上的验证
var PersonPrototype = Person.prototype
console.log(PersonPrototype)
console.log(PersonPrototype.constructor) // ƒ Person() {}
console.log(PersonPrototype.constructor === Person)
console.log(Person.name) // Person
console.log(PersonPrototype.constructor.name) // Person
// 2.实例对象p
var p = new Person()
console.log(p.__proto__.constructor) // ƒ Person() {}
console.log(p.__proto__.constructor.name) // Person
3. 将方法放到原型上
当我们多个对象拥有共同的值时, 我们可以将它放到构造函数对象的显式原型,由构造函数创建出来的所有对象, 都会共享这些属性
/*
1.什么是函数的显式原型
* 区分和对象原型区别
2.函数的原型的作用
* 在通过new操作创建对象时, 将这个显式原型赋值给创建出来对象的隐式原型
3.案例Person, 将所有的函数定义放到了显式原型上
*/
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")
// }
// this.studying = function() {
// console.log(this.name + " studying")
// }
}
// 当我们多个对象拥有共同的值时, 我们可以将它放到构造函数对象的显式原型
// 由构造函数创建出来的所有对象, 都会共享这些属性
Student.prototype.running = function() {
console.log(this.name + " running")
}
Student.prototype.eating = function() {
console.log(this.name + " eating")
}
// 1.创建三个学生
var stu1 = new Student("ximingx", 18, 111)
var stu2 = new Student("kobe", 30, 112)
var stu3 = new Student("james", 18, 111)
// 隐式原型的作用
// 1> stu1的隐式原型是谁? Student.prototype对象
// 2> stu1.running查找:
// * 先在自己身上查找, 没有找到
// * 去原型去查找
stu1.running()
stu2.eating()
4. 创建对象的内存表现
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var p1 = new Person("why", 18)
var p2 = new Person("kobe", 30)
// 进行操作
p1.running()
p2.running()
5. 重写原型对象
如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象
每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性
重写整个原型对象相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了
function Person() {
}
console.log(Person.prototype)
// 在原有的原型对象上添加新的属性
// Person.prototype.message = "Hello Person"
// Person.prototype.info = { name: "哈哈哈", age: 30 }
// Person.prototype.running = function() {}
// Person.prototype.eating = function() {}
// console.log(Person.prototype)
// console.log(Object.keys(Person.prototype))
// 直接赋值一个新的原型对象
Person.prototype = {
message: "Hello Person",
info: { name: "哈哈哈", age: 30 },
running: function() {},
eating: function() {},
// constructor: Person
}
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Person
})
console.log(Object.keys(Person.prototype))
如果希望constructor指向Person,那么可以手动添加, 上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true,默认情况下, 原生的constructor属性是不可枚举的,如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数
3. 原型链的查找顺序
我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取
[Object: null prototype] {} 原型属性已经指向的是null,也就是已经是顶层原型
// 1.{}的本质
// var info = {}
// 相当于
// var info = new Object()
// console.log(info.__proto__ === Object.prototype)
// 2.原型链
var obj = {
name: "why",
age: 18
}
// 查找顺序
// 1.obj上面查找
// 2.obj.__proto__上面查找
// 3.obj.__proto__.__proto__ -> null 上面查找(undefined)
// console.log(obj.message)
// 3.对现有代码进行改造
obj.__proto__ = {
// message: "Hello aaa"
}
obj.__proto__.__proto__ = {
message: "Hello bbbb"
}
obj.__proto__.__proto__.__proto__ = {
message: "Hello ccc"
}
console.log(obj.message)
我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
4. 原型链实现的继承
// 定义Person构造函数(类)
function Person(name, age, height, address) {
this.name = name
this.age = age
this.height = height
this.address = address
}
Person.prototype.running = function() {
console.log("running~")
}
Person.prototype.eating = function() {
console.log("eating~")
}
// 定义学生类
function Student(name, age, height, address, sno, score) {
this.name = name
this.age = age
this.height = height
this.address = address
this.sno = sno
this.score = score
}
// 创建一个父类的实例对象(new Person()), 用这个实例对象来作为子类的原型对象
var p = new Person("why", 18)
Student.prototype = p
Student.prototype.studying = function() {
console.log("studying~")
}
原型链继承的弊端
第一,我们通过直接打印对象是看不到这个属性的
第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
第三,不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)
5. 借用构造函数继承
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数
因为函数可以在任意的时刻被调用,因此通过apply()和call()方法也可以在新创建的对象上执行构造函数
// 定义Person构造函数(类)
function Person(name, age, height, address) {
this.name = name
this.age = age
this.height = height
this.address = address
}
Person.prototype.running = function() {
console.log("running~")
}
Person.prototype.eating = function() {
console.log("eating~")
}
// 定义学生类
function Student(name, age, height, address, sno, score) {
// 重点: 借用构造函数
Person.call(this, name, age, height, address)
// this.name = name
// this.age = age
// this.height = height
// this.address = address
this.sno = sno
this.score = score
}
var p = new Person("why", 18)
Student.prototype = p
组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数
除此之外所有的子类实例事实上会拥有两份父类的属性, 一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__
里面)
function Person(name, age, height) {}
function Student() {}
inherit(Student, Person)
// 1.之前的做法: 但是不想要这种做法
// var p = new Person()
// Student.prototype = p
// 2.方案一:
var obj = {}
// obj.__proto__ = Person.prototype
Object.setPrototypeOf(obj, Person.prototype)
Student.prototype = Person.prototype
// 3.方案二:
function F() {}
F.prototype = Person.prototype
Student.prototype = new F()
// 4.方案三:
var obj = Object.create(Person.prototype)
console.log(obj.__proto__ === Person.prototype)
Student.prototype = obj
6. 寄生组合实现继承
创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回
// 创建对象的过程
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
// 寄生式函数
function inherit(Subtype, Supertype) {
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
// 寄生组合式继承
// 原型链/借用/原型式(对象之间)/寄生式函数
function Person(name, age, height) {
this.name = name
this.age = age
this.height = height
}
Person.prototype.running = function() {
console.log("running~")
}
Person.prototype.eating = function() {
console.log("eating~")
}
function Student(name, age, height, sno, score) {
Person.call(this, name, age, height)
this.sno = sno
this.score = score
}
inherit(Student, Person)
Student.prototype.studying = function() {
console.log("studying")
}
// 创建实例对象
var stu1 = new Student("why", 18, 1.88, 111, 100)