JS:原型-原型链-ES5继承
1. 普通对象的原型
- 什么是对象原型?
- 获取对象原型的方法?
- 对象原型的作用?
原型:JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象
- 获取对象原型 两种方法:
- 不能这样获取obj.[[prototype]]
- 这样获取obj.__proto__
使用的时候需要判断一下。
这个是浏览器实现的。对象原型,也可以称为隐式原型。因为一般不常用。
下文为了方便使用__proto__
3. 标准的方法Object.getPrototypeOf(obj) 推荐使用标准的方法
- 疑问:这个原型有什么用呢?
当我们通过[[get]]的方式获取一个属性对应的value时,
它会优先在自己的对象中查找,如果找到直接返回。
如果没有找到,那么会在原型对象中找。
2. 函数对象的原型
-
什么是函数的显式原型,和对象原型的区别?
-
函数原型的作用?
-
案例Student,将所有的函数定义放到了显式原型上。
-
将函数看成是一个普通对象的时候,具备[[prototype]]属性。
-
将函数看成是一个函数时,具备prototype属性。
对象没有prototype,可以称为显式原型,一般直接用的。
是因为函数才有prototype,不是因为是对象。
对象的原型作用:
- 查找key对应的value时,会找到原型身上。
函数原型的作用:
-
用来构建对象时,给对象设置隐式原型的。
再看new操作符
-
创建一个空对象
-
将这个空对象赋值给this
-
将函数的显式原型赋值给这个对象作为它的隐式原型。
-
执行函数体中的代码
-
将这个对象默认返回。
function Foo() { // 1. 创建空的对象 // 2. 将Foo的prototype(显式)赋值给空的对象的__proto__(隐式) } console.log(Foo.prototype)
var f1 = new Foo() var f2 = new Foo()
console.log(f1.proto) console.log(f1.proto === Foo.prototype) // true console.log(f1.proto === f2.proto) // true 构造出来的对象的隐式原型都是同一个对象Foo.prototype。
new第三步意味着什么?
意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
疑问:这个同一个对象有什么作用?
当我们多个对象用于共同的值时,我们可以将它放到构造函数对象的显示原型上,
由构造函数创建出来的所有对象,都会共享这些属性。
function Studnet(name, age) {
this.name = name
this.age = age
this.studying = function() {
console.log(this.name + " is studying")
}
}
var stu1 = new Student("Kobe", 18)
var stu2 = new Studnet("Curry", 18)
console.log(stu1.studying === stu2.studying)
// false,重新创建了函数,我们不需要不同的函数。
// 因为隐式原型就是Student.prototype
Student.prototype.studying = function() {
console.log(this.name + " is studying")
}
stu1.studying()
// 通过stu1隐式原型找到studying方法。this指向stu1(隐式绑定)
3. 函数原型的属性
-
constructor
-
操作属性
-
结合内存图理解操作
function Person(name, age) { this.name = name this.age = age }
Person.prototype.running = function() { console.log("running~") }
console.log(Person.prototype.constructor === Person) // true
var p1 = new Person("Kobe", 18) var p2 = new Person("Curry", 30)
// 进行操作 console.log(p1.name) console.log(p2.name)
p1.running() p2.running()
// 新增属性 Person.prototype.address = "中国" p2.isAdmin = true // 添加p2的属性,原型对象上无isAdmin
// 获取属性 console.log(p1.address)
console.log(p1.isAdmin) // undefined console.log(p2.isAdmin)
// 修改address p1.address = "广州" console.log(p2.address) // 中国
总结:
- 构造函数上面有prototype属性,它指向构造函数的显式原型对象,显式原型对象中有constructor属性,指向了构造函数。
- 所有由new关键字创建的对象的[[prototype]]属性,指向构造函数的显式原型对象,在显式原型对象中每个添加的方法属性,指向对应的方法对象。
4. 重写原型对象
使用场景:如果我们需要在原型上添加过多的属性,通常重写原型对象。
Person.prototype.message = "Hello World"
Person.prototype.info = {name: "wang", age: 18}
Person.prototype.running = function() {console.log("running~")}
/// 让构造函数的显式原型不再指向原有的默认指向的原型对象,指向一个新的空对象
Person.prototype = {
message: "Hello World",
info: {name: "wang", age: 18},
running: function() {console.log("running~")},
constructor: Person
}
console.log(Object.keys(Person.prototype))
// 这个constructor是可以枚举的。直接添加不可枚举,如果需要一致
Object.defineProperty(Person.prototype, "constructor", {
configurable: true,
enumerable: false,
value: Person,
writable: true
})
如果不写constructor: Person,这个constructor属性,会指向Object构造函数。
constructor默认属性描述符:
5. 面向对象的三大特性
- 概念
- 继承的思想
JavaScript支持面向对象编程也支持函数式编程。
面向对象编程 -->编程方式(范式)
怎么去创建一个类,根据这个类创建对象,然后操作。
-
面向对象三大特性
-
封装:将属性和方法封装到一个类中,可以称之为封装的过程。
-
继承:可以减少重复的代码量,也是多态的前提(纯面向对象中)。
-
多态:不同的对象在执行时表现出不同的形态。
-
将普通变量封装到一个对象 ,将一个对象封装到一个类。最终抽象为类。
-
不同的类,具有相同的属性和方法,可以抽取到一个父类中。子类继承父类就可以实现父类的属性。
-
多态在js中更多表现是一个“鸭子类型”
function Student(name, age, score) { this.name = name this.age = age this.score = score } Student.prototype.running = function() {} Student.prototype.fighting = function() {}
function Teacher(name, age, salary) { this.name = name this.age = age this.salary = salary } Teacher.prototype.running = function() {} Teacher.prototype.teaching = function() {}
// 抽取父类
要实现继承,es5中需要用到原型链。
6. 对象的原型链
- 定义
- 默认原型链,自定义原型链
- 查找顺序,结合图
- 补充
定义:
- 从一个对象上获取属性,如果当前对象中没有获取到就会去它的原型上获取。
-
默认原型链
-
自定义的原型链
var obj = { name: "wang", age: 18 } console.log(obj.message) obj.proto = { // message: "hello a" } obj.proto.proto = { // message: "hello b" } obj.proto.proto.proto = { message: "hello c" } console.log(obj.message) console.log(obj.proto.proto.proto.proto === Object.prototype) console.log(obj.proto.proto.proto.proto.proto === null)
查找顺序
- obj上面查找
- obj.__proto__上面查找
- obj.__proto__.__proto__==>null 上面查找 undefined
补充:
原型链的顶层:null
所有构造函数继承Object构造函数。即Object是所有类的一个父类
7. 原型链实现继承ES5继承
function Student(name, age, score) {
this.score = score
}
Student.prototype.fighting = function() {}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var stu1 = new Student()
// 需要stu1调用running,自身的原型上去找,没有,再去原型的原型上找,指向的是Person.prototype
// 如何让二者关联?
方式一:父类的原型直接赋值给子类的原型 (错误的做法)
缺点:父类和子类共享一个原型对象,修改一个,另外一个也被修改。
Student.prototype = Person.prototype
Student.prototype.studying = function() {
console.log("studying~")
}
var p = new Person()
p.studying() // 可以调用
方式二:创建一个父类的实例对象,用这个实例作为子类的原型对象。
var p1 = new Person("wang", 18)
Student.prototype = p1
解决了方式一缺点,
缺点:
继承了实例对象的属性。
如果删除子类中的赋值属性的代码,也会打印出属性,
但是打印的是继承父类的属性。
原型链继承的弊端:
- 某些属性其实是保存在p1对象上的;
- 直接打印对象看不到这个属性。
- 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题。
- 不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没办法定制化)。
8. 借用构造函数实现-属性继承
- 为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数)
做法:在子类的构造函数内部调用父类的构造函数
function Student(name, age, score) {
Person.call(this, name, age) // 借用构造函数,调用时是Student对象
this.score = score
}
Student.prototype.fighting = function() {}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log("running~")
}
var p1 = new Person("wang", 18)
Student.prototype = p1
var stu1 = new Student("Kobe", 18, 100)
console.log(stu1.age)
console.log(stu1.name)
console.log(stu1)
加上原型链继承就是组合借用继承。
缺点:
组合继承最大的问题就父类的构造函数至少会被调用两次。
事实上,所有的子类实例事实上会拥有两份的父类属性。一个是本身,一个是原型上。
9. 最终继承方案-寄生组合式继承
9.1 原型式继承
-
原型式继承的渊源:
-
这种模式需要从 Douglas Crockford( 著名的前端大师,JSON的创立者 )在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript
-
它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的
-
最终的目的
student对象的原型指向了person对象
9.2 寄生式继承
-
寄生式(Parasitic)继承
-
寄生式继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
-
寄生式继承的思路是结合原型类继承和工厂模式的一种方式;
-
即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
需要创建一个类似于p1的对象满足
-
必须创建出来一个对象
-
将这个对象的隐式原型属性指向父类的显式原型属性
-
将这个对象赋值给子类的显式原型
function Person() {
} function Student() {
} // 之前的做法 // var p = new Person() // Studnet.prototype = p // 方案一 // var obj = {} // Object.setPrototypeOf(obj, Person.prototype) // Student.prototuype = obj
// 方案二 // function F() {} // F.prototype = Person.prototype // Student.Prototype = new F()
// 方案三 // var obj = Object.create(Person.prototype) // console.log(obj.proto === Person.prototype) // Student.prototype = obj
// 封装工具函数 function inherit(Subtype, Supertype) { Subtype.prototype = Object.create(Supertype.prototype) Object.defineProperty(Subtype.prototype, "constructor", { enumerable: false, configurable: true, wriable: true, value: Subtype }) }
inherit(Student, Person)
// 考虑兼容性问题 Object.create function createObject(o) { function F() {} F.prototype = o return new F() }
将Subtype和Supertype联系在一起,称为寄生式函数。
整个过程称为寄生组合式继承。
9.3 寄生组合式继承
寄生组合式继承:
-
原型链
-
借用构造函数
-
原型式(对象之间)
-
寄生式函数
function inherit(Subtype, Supertype) { Subtype.prototype = Object.create(Supertype.prototype) Object.defineProperty(Subtype.prototype, "constructor", { enumerable: false, configurable: true, wriable: true, value: Subtype }) } function Person(name, age) { this.name = name this.age = age } Person.prototype.running = function() { console.log("running~") }
function Student(name, age, score) { Person.call(this, name, age) this.score = score }
inherit(Student, Person) Student.prototype.studying = function() { console.log("studying~") }
var stu1 = new Student("Kobe", 30, 10000000) console.log(stu1) stu1.studying() stu1.running()
完美解决
11. 原型-寄生思想的来源
Douglas Crockford研究对象之间的继承。实际上实现继承并不好。
var obj = {
name: "wang",
age: 18
}
// 原型式继承
var info = {}
info.__proto__ = obj
// 如果创建多个继承的对象,出现大量重复代码
// 使用原型式继承 __proto__会出现浏览器兼容性问题,因此没有用
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
// var info1 = createObject(obj)
// info1.height = 188
// var info2 = createObject(obj)
// info2.height = 199
// 目前的代码有弊端,没有自己特有的属性,需要单独添加。
// 寄生式函数
function createInfo(o, name, age, height) {
var newObj = createObject(o)
newObj.name = name
newObj.age = age
newObj.height = height
return newObj
}
// 创建一系列对象
var info1 = createInfo(obj, "Kobe", 18, 222)
console.log(info1)
10. 原型继承关系
内存图:
转载自:https://juejin.cn/post/7144689665562378277