JS原型链和继承
认识对象的原型
[[Get]]:JS的存取描述符——get方法,在获取对象属性时会自动调用
-
JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的属性指向另外一个对象
-
[[prototype]]指向的对象:
- 当我们通过引用对象的属性名来获取属性值的时候,会触发[[Get]]
- [[Get]]会首先检查该对象是否有对应的属性,如果有就直接使用
- 如果对象中没有该属性,那么会访问内置属性[[prototype]]所指向的对象,在其中再次检查是否有对应的属性,有就直接用,没有则返回undefined
// JavaScript var obj = { name: "coder", age: 18 } obj.__proto__ = { address: "广州市" } console.log(obj) // {name: "coder",age: 18} console.log(obj.address) // 广州市
-
如果通过字面量直接创建一个对象(如:var a={}),这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 有。只要是对象都会有这样的一个内置属性
- 获取方式有两种:
- 通过对象的
__proto__
属性可以获取到(但是这个属性是早期浏览器自己添加的,开发中尽量避免使用,存在一定的兼容性问题) - 通过 Object.getPrototypeOf 方法可以获取到
- 通过对象的
// JavaScript // 承接上文代码 console.log(obj.__proto__) // {address: '广州市'} console.log(Object.getPrototypeOf(obj)) // {address: '广州市'}
什么是原型链?
- [[Get]]首先会在当前对象中查找属性
- 如果没有找到,就会去原型链(
__proto__
)对象上查找 - 在找到顶层原型时,便不会再向继续查找了
// JavaScript
// 承接上文代码
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
target: "excellent"
}
console.log(obj.target) // excellent
console.log(obj.__proto__.__proto__.__proto__.__proto__ === Object.prototype) // true(顶层原型,后面说)
顶层原型是什么?
到底是找到哪一层对象之后停止继续查找了呢?—顶层
// JavaScript
var obj1 = {
name: "zzwi"
}
console.log(obj1.__proto__) // { ... }
console.log(obj1.__proto__.__proto__) // null
obj1.__proto__
便是顶层原型对象
顶层原型来自哪里?
// JavaScript
var obj2 = {
name: "bill",
age: 21
}
console.log(Object) // ƒ Object() { [native code] }
console.log(obj2.__proto__) // { ... }
console.log(Object.prototype) // { ... }
// 新创建的obj2和原生Object函数 的原型属性指向共同的原型对象
console.log(obj2.__proto__ === Object.prototype) // true
// Object函数的原型对象的constructor指向Object函数
console.log(Object.prototype.constructor) // ƒ Object() { [native code] }
// Object函数的原型对象已是顶层
console.log(Object.prototype.__proto__) // null
// Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。
console.log(Object.getOwnPropertyDescriptors(Object.prototype)) // { ... }
从上面的Object原型我们可以得出一个结论:
原型链最顶层的原型对象就是Object的原型对象
顶层原型Object的特性:
- 该对象有原型属性,但是它的原型属性指向的是null
- 该对象上有很多默认的属性和方法
// JavaScript
var p1 = {
name: "zzwi",
age: 21
}
var p2 = {
address: "广州市"
}
p1.__proto__ = p2
console.log(p1.address) // 广州市
代码逻辑如图所示
构造函数原型
构造函数也可称为类
代码:
// JavaScript
function Person(){}
var p = new Person()
console.log(Person.prototype.__proto__ === Object.prototype) // true
Person构造函数的prototype指向的原型对象拥有它自己的__proto__
并指向了顶层原型
new一个对象p的过程
-
在内存中创建一个对象 var moni = {}
-
this的赋值 this = moni
-
将Person函数的显式原型prototype赋值给创建出来的对象的隐式原型
moni.__proto__=Person.prototype
面向对象三大特性:
- 封装
- 继承
- 多态
这里核心讲继承
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
在JavaScript中需要利用原型链的机制实现一下继承
原型链的继承
父类与子类
父类放置公共属性和方法,减少重复代码、实现代码复用
子类放置特有属性和方法,使代码更为灵活
// JavaScript
// 父类:公共属性和方法
function Person() {
this.name = "coder"
}
Person.prototype.running = function() {
console.log(this.name + " running~")
}
// 子类:特有属性和方法
function Student() {
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
var stu = new Student()
console.log(stu.name) // coder
stu.running() // coder running
stu.studying() // coder studying
p是父类创建的对象,原型属性__proto__
由父类Person的prototype赋值,因此也指向Person的原型对象
Student的prototype赋值为p对象,Student.prototype.__proto__ === Person.prototype
就此和Person建立原型链关系,成为Person的子类
所以,由Student创建的对象stu,其中stu.__proto__ = Student.prototype
,查找name属性和running方法时,能够沿着原型链找到父类Person内的name属性和原型对象的running方法
Object是所有类的父类
如下代码所示
// JavaScript
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.running = function() {
console.log(this.name + "running~")
}
var p1 = new Person("kaze", 18)
console.log(p1) // Person {name: 'kaze', age: 18}
console.log(p1.valueOf()) // Person {name: 'kaze', age: 18}
console.log(p1.toString()) // [object Object]
原型链继承的弊端
承接上文 父类与子类 的代码
// JavaScript
// 1. 第一个弊端:打印stu对象,继承的属性是看不到的
console.log(stu) // Student {sno: 111}
// 2. 第二个弊端:创建出来的两个stu对象
var stu1 = new Student()
var stu2 = new Student()
// 直接修改对象上的属性,是给本对象添加了一个新属性
stu1.name = "kaze"
// stu1.friends = []
console.log(stu1.name) // kaze
console.log(stu2.name) // coder
// 获取引用,修改引用的属性值,会相互影响
stu1.friends.push("kaze")
console.log(stu2.friends) // ['kaze']
// 3. 第三个弊端:在前面实现类的过程中都没有传递参数
var stu3 = new Student("bill", 21)
借用构造函数继承
代码如下:
// JavaScript
// 父类:公共属性和方法
function Person(name,age,friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.running = function() {
console.log(this.name + " running~")
}
// 子类:特有属性和方法
function Student(name, age, friends, sno) {
// 原型链继承弊端的解决方法
Person.call(this, name, age, friends)
this.sno = sno
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
var stu = new Student("coder",18,["trump"],1)
console.log(stu) // Student {name: 'coder',age: 18,friends: ['trump'],sno: 1}
var stu1 = new Student("zzwi",20,["kaze"],111)
var stu2 = new Student("bill",19,["kobe"],222)
stu1.friends.push("black")
console.log(stu1.friends) // ['kaze','black']
console.log(stu2.friends) // ['kobe']
使用call方法, Person.call(this, name, age, friends)
,这就是原型链继承弊端的解决方法
如图
借用构造函数继承的弊端
强调: 借用构造函数也是有弊端:
1.第一个弊端: Person函数至少被调用了两次
2.第二个弊端: stu的原型对象即p对象上会多出一些属性, 但是这些属性没有存在的必要
原型式继承函数
代码如下
// JavaScript
var obj = {
name: 'zzwi',
age: 18
}
// 原型式继承函数
// 方法1
function createObject1(o) {
var newObj = {}
Object.setPrototypeOf(newObj,o)
return newObj
}
var info1 = createObject1(obj)
console.log(info1.__proto__) // {name:'zzwi',age:18}
// 方法2
function createObject2(o) {
function Fn() {}
Fn.prototype = o
var newObj = new Fn()
return newObj
}
var info2 = createObject1(obj)
console.log(info2.__proto__) // {name:'zzwi',age:18}
// 方法3
var info = Object.create(obj)
console.log(info.__proto__) // {name:'zzwi',age:18}
最终目的
info、info1、info2对象的原型都指向了obj对象
寄生式继承
寄生式继承是结合原型式继承和工厂模式的一种方式
代码如下:
// JavaScript
var personObj = {
running: function() {
console.log("running~");
}
}
// 工厂模式
function createStudent(name) {
var stu = Object.create(personObj) // 原型式继承
stu.name = name
stu.studying = function() {
console.log("studying~");
}
return stu
}
var stuObj1 = createStudent("coder")
console.log(stuObj1.__proto__) // {running: ƒ}
接下来便是最终方案:
寄生组合式继承
现在我们来回顾一下之前提出的比较理想的组合继承
-
组合继承是比较理想的继承方式, 但是存在两个问题:
-
问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
-
问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.
事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉.
-
你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中 的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.
-
这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
能不能直接让子类型的原型对象 = 父类型的原型对象呢?
-
不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型 也会被修改.
-
我们使用前面的寄生式思想就可以了
代码如下:
// JavaScript
// 原型式继承
function createObject(o) {
function Fn() {}
Fn.prototype = o
return new Fn()
}
// 继承函数封装_工厂模式
function inheritPrototype(SubType,SuperType) {
SubType.prototype = createObject(SuperType.prototype)
// 将stu对象的类型值从Person修改为Student
Object.defineProperty(SubType.prototype,"constructor",{
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}
function Person(name,age,friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.running = function() {
console.log("running~")
}
function Student(name,age,friends,sno,score) {
Person.call(this,name,age,friends,sno,score)
this.sno = sno
this.score = score
}
inheritPrototype(Student,Person)
Student.prototype.studying = function() {
console.log("studying~")
}
var stu = new Student("zzwi",18,["bill"],111,100)
console.log(stu) // Student { ... }
stu.running() // running~
console.log(Person.prototype.studying) // undefined
对象的方法补充
hasOwnProperty
- 对象是否有某一个属于自己的属性(不是在原型上的属性)
in/for in 操作符
- 判断某个属性是否在某个对象或者对象的原型上
instanceof
- 用于检测构造函数的pototype,是否出现在某个实例对象的原型链上
isPrototypeOf
- 用于检测某个对象,是否出现在某个实例对象的原型链上
代码如下
// JavaScript
var obj = {
name: "bill",
age: 18
}
var info = Object.create(obj,{
address: {
enumerable: true,
value: "广州市"
}
})
// hasOwnProperty方法判断
// 属性或方法在当前对象中则返回true,在原型中便返回false
console.log(info.hasOwnProperty("address")) // true
console.log(info.hasOwnProperty("name")) // false
// ================================================
// in 操作符:不管在当前对象还是原型中返回的都是true
console.log("address" in info) // true
console.log("name" in info) // true
for(var key in info) {
console.log(key) // address name age
}
// ================================================
// instanceof
// 用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
function Person() {}
function Student() {}
Student.prototype = new Person()
Student.prototype.studying = function() {
console.log("studying~");
}
var stu = new Student()
console.log(stu instanceof Student) // true
console.log(stu instanceof Person) // true
console.log(stu instanceof Object) // true
// 用于检测某个对象,是否出现在某个实例对象的原型链上
console.log(Person.prototype.isPrototypeOf(stu)) // true
console.log(Student.isPrototypeOf(stu)) // false
对象-函数-原型之间的关系
代码如下:
// JavaScript
var obj = {
name: "why"
}
console.log(obj.__proto__) // { ... }
// 对象里面是有一个__proto__对象: 隐式原型对象
// Foo是一个函数, 那么它会有一个显式原型对象: Foo.prototype
// Foo.prototype来自哪里?
// 答案: 创建了一个函数, Foo.prototype = { constructor: Foo }
// Foo是一个对象, 那么它会有一个隐式原型对象: Foo.__proto__
// Foo.__proto__来自哪里?
// 答案: new Function() Foo.__proto__ = Function.prototype
// Function.prototype = { constructor: Function }
// Function.prototype === Function.__proto__
// var Foo = new Function()
function Foo() {
}
console.log(Foo.prototype === Foo.__proto__) // false
console.log(Foo.prototype.constructor) // Foo(){}
console.log(Foo.__proto__.constructor) // Function(){...}
console.log(Function.prototype === Function.__proto__) // true
var foo1 = new Foo()
var obj1 = new Object()
console.log(Object.getOwnPropertyDescriptors(Function.__proto__)) // {...}