目录
一、对象的原型
1.1 概念
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]] ,这个特殊的对象可以指向另外一个对象。
1.2 作用
- 当通过引用对象的属性key来获取一个value时,它会触发[[Get]]的操作
- 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
- 如果对象中没有该属性,那么会访问对象[[prototype]] 内置属性指向的对象上的属性
💠只要是对象都会有这样的一个内置属性!
1.3 获取方式
🔹方式一:(不常用)
通过对象的__proto__属性可以获取到 (早期浏览器自己添加的,存在一定的兼容性问题)
🔹方式二:(常用)
通过Object.getPrototypeOf方法可以获取到
二、函数的原型 prototype
2.1 概念
所以的函数都有一个prototype的属性(注意:不是__proto__)
(🏷️箭头函数没有原型)
💠虽然函数也是一个对象,但是对象上面没有prototype的属性;
💠因为是函数,所以才有这个特殊的属性;
💠(隐式全都要,显式只有函数有)
2.2 作用
用来构建对象时,给对象设置隐式原型的
2.2.1 new操作符
步骤:1. 在内存中创建一个新的对象(空对象)
2. 这个对象内部的[ [ prototype ] ] 属性会被赋值为该构造函数的prototype属性
因此,也就意味着通过Person构造函数创建出来的所有对象的[ [ prototype ] ] 属性都指向Person.prototype
function Person() {
}
//函数的显式原型
console.log(Person.prototype)
//new操作
var p1 = new Person()
var p2 = new Person()
var p3 = new Person()
//实例的隐式原型 指向 函数的显式原型
console.log(p1.__proto__)
console.log(p1.__proto__ === Person.prototype) // true
//实例的隐式原型都共同指向构造函数的显式原型
console.log(p1.__proto__ === p3.__proto__) //true
2.3 constructor属性
函数的显式原型上都会存在一个属性叫做constructor,这个constructor指向当前的函数对象
function Person() {
}
console.log(Person.prototype.constructor) // [Function: Person]
console.log(p1.__proto__.constructor) // [Function: Person]
console.log(p1.__proto__.constructor.name) // Person
三、面向对象的特征—继承
3.1 面向对象三大特性
封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态的前提(纯面向对象中)
- 多态:不同的对象在执行时表现出不同的形态
3.2 继承是做什么的?
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
继承也是多态的前提。
3.3 JavaScript中如何实现继承
使用JavaScript原型链的机制
四、JavaScript原型链
从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取
五、Object
5.1 Object的原型
那么什么地方是原型链的尽头:Object的原型对象
console.log(obj.__proto__.__proto__.__proto__) //[Object: null prototype] {}
这时会发现打印出来的结果是 [Object: null prototype] {},事实上这个原型就是最顶层的原型了
💠从Object直接创建出来的对象的原型都是 [Object: null prototype] {}
🗯️[Object: null prototype] {} 原型有什么特殊吗?
- 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了
- 特殊二:该对象上有很多默认的属性和方法
5.2 Object是所有类的父类
从上面Object原型可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
六、实现继承的方式(重点)
JavaScript想要实现继承的目的:重复利用另一个对象的属性和方法
6.1 通过原型链实现继承
主要代码:子类构造函数.prototype = new 父类构造函数()
//1.定义父类构造函数
function Person() {
this.name = "xiaomimg"
}
//2.父类原型上添加内容
Person.prototype.running = function() {
cosole.log(this.name + "running~")
}
//3.定义子类构造函数
function Student() {
this.sno = 111
}
//4.创建父类对象,并且作为子类的原型对象
var p new Person()
Student.prototype = p
//5.在子类原型上添加内容
Student.prototype.studying = function() {
cosole.log(this.name + "studying~")
}
目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数
🔺注意:步骤4和步骤5不可以调整顺序,否则会有问题
原型链继承的弊端
缺点:某些属性其实是保存在p对象上的
- 第一,通过直接打印对象是看不到这个属性的
- 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题
- 第三,不能给Person传递参数(让每个stu有自己的属性),因为这个对象是一次性创建的(没有办法定制化)
优点:好理解,逻辑清晰,可以继承父类属性和方法
6.2 借用构造函数继承
主要代码:在子类构造函数中使用 call() / apply()方法调用父类构造函数
因为函数可以在任意时刻被调用,所以通过apply()和call()方法也可以在新创建的对象上执行构造函数
function Student(name, friends, sno) {
Person.call(this, name, friends)
this.sno = sno
}
Student.prototype = Person.pertotype
优缺点:
优点:可以先父类传参,定制对象,且不会造成属性共享的问题
缺点:
- 虽然可以继承属性和方法,但方法必须写在构造函数中,创建出来的对象都自带方法,无法进行方法的复用,浪费空间
- 父类的原型上绑定的属性和方法无法被子类使用
6.3 组合继承(仍不够完美,但是基本上没用问题了)
组合继承 = 原型链继承 + 借用构造函数继承
主要代码:1. 子类构造函数.prototype = new 父类构造函数()
2. 在子类构造函数中使用 call | apply 调用父类构造函数
优缺点:
优点:用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,弥补了各自的缺点
缺点:
- 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数
(1)一次在创建子类原型的时候
(2)另一次在子类构造函数内部(也就是每次创建子类实例的时候)
- 所以的子类实例事实上会拥有两份父类的属性
一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__里面)。
🏷️当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的。
//工具函数
// function createObject(o) {
// function F() {}
// F.prototype = o
// return new F()
// }
//将Subtype和Supertype联系在一起
//寄生式函数
function inherit(Subtype, Supertype) {
//不担心兼容性的写法
// Subtype.prototype = Object.create(Supertype.prototype)
Subtype.prototype = createObject(Supertype.prototype)
Object.defineProperty(Subtype.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Subtype
})
}
//定义Person的构造函数
function Person(name, age, height, address) {}
Person.prototype.running = function() {
console.log('person running')
}
function Student(name, age, height, address, sno, score) {}
inherit(Student, Person);
console.log(new Student());
(new Student()).running()
/**
* 满足什么条件
* 1.必须创建出来一个对象
* 2.这个对象的隐式原型必须指向父类的显式原型
* 3.将这个对象赋值给子类的显式原型
*/
/*
之前的做法:这个不是我们想要的 不推荐
*/
// var p = new Person() // p.__proto__ === Person.prototype
// Student.prototype = p
//方案一:
// var obj = {} // __proto__ Object
// // obj.__proto__ = Person.prototype (可能存在兼容性问题, 所以用下面的方法)
// Object.setPrototypeOf(obj, Person.prototype)
// Student.prototype = obj
//方案二:
// 兼容性最好
// function F() {}
// F.prototype = Person.prototype
// Student.prototype = new F()
//方案三:
// var obj = Object.create(Person.prototype)
// console.log(obj.__proto__ === Person.prototype) //true
// Student.prototype = obj
6.4 寄生组合式继承(最终继承方案)
寄生式继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
寄生式继承的思路就是结合原型类继承和工厂模式的一种方式,即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回。
寄生组合式继承 = 原型链继承 + 借用构造函数继承 + 寄生函数
优缺点:
优点:这是最成熟的方法,也是现在库实现的方法
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
}
function createObject(aaa) {
function F() {}
F.prototype = aaa
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
})
}
inherit(Student, Person)
Student.prototype.studying = function() {
console.log("studying")
}
var stu1 = new Student("ming", 18, 88, 100, 20)
console.log(stu1)
console.log(stu1.__proto__)
console.log(Student.prototype)
console.log(Person.prototype)
输出结果:
七、原型继承关系
7.1 解释
1. 系统自带的构造函数 function Object()
创建对象无论是采用字面量的形式还是实例化Object构造函数来创建,本质上字面量的形式还是会从Object构造函数来进行创建。
字面量:var obj1 = {}
实例化:var obj2 = new Object()
(1)当执行 var obj2 = new Object() 时,会将构造函数的prototype复制给创建对象的__proto__ ,即 obj2.__proto__ = Object.prototype ,所以图中01,02对象的__proto__指向Object.prototype。
(2)Object构造函数原型就是本身的原型 Object.prototype
Object.prototype中的constructor指向本身构造函数Object
(3)因为Object.prototype是原型对象,是对象就会有__proto__,又因为它是最顶层的,所有对象的起源都源自于它,在它之上就没有原型了,所以它的__proto__指向null
2. 自己定义的构造函数 function Foo()
(1)由1(1)可知,图中f1,f2对象的__proto__指向 Foo.prototype
(2)prototype与constructor指向同1(2)
(3)因为函数既是函数也是对象,所以是对象的话就会有__proto__,是函数的话就会有prototype。又在这个函数被创建的时候,函数里面就会创建一个叫prototype的属性,所以会产生Foo.prototype。又它的值是一个对象,所以它内部创建肯定是通过实例化了Object构造函数创建所得。所以它的指向毫无疑问是指向Object.prototype
Foo.prototype = new Object()
Foo.prototype.__proto__ = Object.prototype
3. 系统自带的构造函数 function Function()
创建函数也可以通过创造函数来创建,和创建对象原理一样
var fn = new Function()
(1)Function的prototype和constructor指向同1(2)一致
(2)Function.prototype指向Object.prototype和2(3)中Foo.prototype指向原理一致
作为函数来说,有自己的显式原型prototype对象,显式原型对象相当于是被new Object() 创建出来的,所以又有自己的__proto__,指向Object的显式原型 Object.prototype
4. 构造函数本身的__proto__指向
对象的隐式原型指向构造函数的显式原型
实例对象 -> new 构造函数
函数对象 -> new Function
原型对象 -> new Object
(1)Foo函数的__proto__ -> Function.prototype
function Foo() {} ===> var Foo = new Function()
所以执行上面代码时,会执行 Foo.__proto__ = Function.prototype
相当于调用了构造函数来创建,所以 Foo()的__proto__ -> Function.prototype
(2)Object函数的__proto__ -> Function.prototype
原理同(1)
function Object() {} ===> var Object = new Function
所以执行上面代码时,会执行 Object.__proto__ = Function.prototype
(3)Function函数的__proto__ -> Function.prototype
相当于Function.__proto__ === Function.prototype (只有这一个满足!)
function Function() {} ===> var Function = new Function()
所以执行上面代码时会执行 Function.__proto__ === Function.prototype
作为对象来说,有自己的隐式原型__proto__对象,比较特殊的事,相当于它被自己创建了,即function Function相当于被 new Function() 创建的,所以function Function().__proto__ 也指向Function函数的显式原型 Function.prototype