前言
一谈JS,离不开的就是对原型和原型链知识的理解了,当我粗略看了一遍《javascript高级程序设计》后,我想我也大概是理解了吧,然后看了许多网上相关文章,WTF???好像懂了又好像没懂,这说并不是各位大佬的文章写的不好,而是知识只有用自己的语言,自己的思想进行总结才更有利于接受和理解。以下内容皆是在下理解的知识总结,通过文章的形式进行输出,用来检验自己是否真正理清了、学会了,如果有错误的地方或者疏漏的地方,请各位小伙伴们多多见谅,并辛苦指出。
概述原型链
不多逼叨,先来看图。
好的,看完之后忘掉它,自己建立一个自己理解的图才是真正理清了。 我先举个道德经的栗子说说自己大概的理解,只是为了方便自己记忆,并不具体指代JS引擎原理。
道德经:无,名天地之始🌱
- JS所有都是从null开始
道德经:道生一
- null之后,JS就生成了一个‘孩子’(原型对象)—— Object.prototype。
Object.prototype.__proto__ === null。
道德经:一生二
- 然后Object.prototype也有了一个‘孩子’——Function.prototype
Function.prototype.__proto__ === Object.prototype
道德经:二生三
- 然后JS发现有了这两个孩子还不够哇,怎么能达到(道德经说的二生三呢?),于是乎Function.prototype又有了一个孩子function Function()。
function Function__proto__ === Function.prototype
道德经:三生万物
- 好了,有了function Function(),那函数就可以通过 new Function() 生成了,那就完成了(三生万物吗?),好像忘记了Object呢? 那咋办呢?JS直接开始定义——JS中的所有东西都是对象,函数也是对象, 而且是一种特殊的对象,所以万物皆对象,所以Obejct是如何生成的呢?对没错,它也是通过new Function()生成的,Object原本就是一个函数,通过new Object()之后实例化后,创建对象。
function Object__proto__ === Function.prototype
我们把上面的理解画成图。
constructor 和 prototype
constructor属性其实就是一个拿来保存自己构造函数引用的属性。现在我们先建立一个构造函数Person,然后通过console.dir(person)显示属性在控制台中显示指定Person对象的属性。
function Person() {}
var person1 = new Person()
var person2 = new Person()
console.dir(Person)
而prototype对象用于放某同一类型实例的共享属性和方法,以减少内存浪费。例如在Person.prototype属性中定义一个属性方法,那么person1和person2 都能共享,就不必再为实例对象分配内存定义相同方法了。
下面看栗子。
由上图得到Person()是个构造函数,然后它有个prototype属性,然后通过这个属性就能访问到原型对象,然后原型对象也可以通过constructor访问到构造函数。
Person.prototype.constructor === Person
再然后Person().prototype的“爸爸”是Object.prototype
Person.prototype.__proto__ === Object.prototype
而Person()的“爸爸”则是Function.prototype
Person.__ptoto__ === Function.prototype
Function.prototype 同理它的构造函数也是 function Function 啦。
Function.prototype.constructor === Function
根据以上所说函数就可以通过 new Function() 生成,对象就通过new Object()生成,所以函数和对象的实例化的“爸爸”肯定就不用说啦! 这时候我们就可以总结出一个小关系图。
person1实例对象是由构造函数通过new创建,这个实例对象上有个属性__proto__属性(“爸爸”)指向了构造函数的原型对象。而构造函数上的prototype属性指向的就是这个原型对象,而原型对象上也有个属性constructor指向的是这个构造函数。
检测
- 所以什么是原型链呢?
- 原型对象和构造函数有什么关系呢?
- 如何获知属性实在原型链上还是对象中?
答案
- 原型链就是JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条。
- 在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
- 同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中还是原型中。object.hasOwnProperty(name) 检查对象自身中是否含有该属性, (name in object) 检查对象中是否含有该属性时,如果对象中没有但是原型链中有,也会返回 true。
小结: Function函数是Object这类内置对象的构造函数,Function函数同时是自己的构造函数。所以Function.prototype位于所有函数的原型链上,Function.prototype又通过_proto_指向Object.prototype,所以所有的函数既是Function的实例又是Object的实例,并分别从这两个内置对象继承了很多属性和方法。
new
官方的说,new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。而上面说到用构造函数创建实例对象需要通过new,那么new是如何实现的呢,new一个函数的时候会发生什么呢?new可以干嘛呢?
不使用new又是什么结果?
let person=new Person();//person是一个对象
let person = Person();//一次普通的函数调用并赋值而已。
new一个函数会发生什么?
- 产生一个全新的对象
- 这个对象会被执行 [[Prototype]] 连接,将这个新对象的 [[Prototype]] 链接到这个构造函数.prototype 所指向的对象
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
实现
function myNew(obj, ...args) {
//Object.create()方法创建一个新对象,
// 使用现有的对象来提供新创建的对象的__proto__。
//构造obj.prototype 的原型 NewObj
const NewObj = Object.create(obj.prototype);
console.log(NewObj);
// 新对象的this绑定到构造函数中this
const result = obj.apply(NewObj, args);
console.log(result);
// 若构造函数返回 非空对象 ,则返回该对象, 否则返回刚刚创建的对象
return (typeof result === 'object' && result !== null) ? result : NewObj;
}
需要注意的是,通过Object.create()方法把新对象的原型对象链接到构造函数的原型链上,并且把新对象绑定到函数调用的this,可以通过apply实现,再然后根据构造函数是否返回了其他对象,把result对象进行返回,或把newObj对象进行返回。
使用栗子
function myNew(obj, ...args) {
//Object.create()方法创建一个新对象,
// 使用现有的对象来提供新创建的对象的__proto__。
//构造obj.prototype 的原型 NewObj
const NewObj = Object.create(obj.prototype);
console.log(NewObj); // Person1 {} , Person2{}
// 新对象的this绑定到构造函数中this
const result = obj.apply(NewObj, args);
console.log(result); // undefined {name:'Bob',age:18}
// 若构造函数返回 非空对象 ,则返回该对象, 否则返回刚刚创建的对象
return (typeof result === 'object' && result !== null) ? result : NewObj;
}
// 构造函数没有返回对象
function Person1(name, age) {
this.name = name;
this.age = age;
}
// 构造函数返回了非空对象
function Person2(name, age) {
this.name = name;
this.age = age;
return {
name: 'Bob',
age: 18
}
}
const Bill = myNew(Person1, 'Bill', 18);
console.log(Bill.name, Bill.age)//Bill 18
const Jay = myNew(Person2, 'Jay', 18)
console.log(Jay.name, Jay.age); // Bob 18
instanceof
instanceof是干嘛用的呢?看看MDN是怎么说的:
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
语法
object instanceof constructor
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上
通过上面的理解知道了JavaScript对象通过prototype指向父类对象,直到指向Object对象为止。那instanceof就是来判断右边构造函数的 prototype 属性在不在左边实例对象的原型链上。它可以顺着原型链一直往上查找,找到了就是true,找不到就是false。
如何实现呢?
- 接收两个参数,左边left为实例对象,右边right为构造函数
- left的“爸爸”(__proto__)为空代表到顶了,已经指到了Object对象,还没找到为false
- left的“爸爸”(__proto__)等于 right.prototype,找到了true
- 顺着原型链不断地找“爸爸”(__proto__),一直到找到了或者到终点了退出
实现
function NewinstanceOf(left, right) {
// 定义“爸爸”
// let proto = Object.getPrototypeOf(left); //同理获取原型对象
let proto = left.__proto__
while (true) {
// “爸爸”为空了,已经到了Object.prototype了
if (proto === null) return false
// 找到了 返回true
if (proto === right.prototype) {
return true
}
// 继续往上找
proto = proto.__proto__
}
}
在代码实现中,需要获取left的原型对象与right进行匹配,匹配成功为true,反之false。而获取left的原型对象有两种方法:left.__proto__,Object.getPrototypeOf(left)。
使用栗子
//... 以上NewinstanceOf(){}代码
// 定义构造函数
function C(){}
function D(){}
var o = new C();
console.log(NewinstanceOf(o,C));// true,因为 Object.getPrototypeOf(o) === C.prototype
console.log(NewinstanceOf(o,D));// false,因为 D.prototype 不在 o 的原型链上
console.log(NewinstanceOf(o,Object));// true,因为 Object.prototype.isPrototypeOf(o) 返回 true
console.log(NewinstanceOf(C.prototype,Object));// true,同上
C.prototype = {};
var o2 = new C();
console.log(NewinstanceOf(o2,C));// true
console.log(NewinstanceOf(o,C));// false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.
D.prototype = new C(); // 继承
var o3 = new D();
console.log(NewinstanceOf(o3,D));// true
console.log(NewinstanceOf(o3,C)); // true 因为 C.prototype 现在在 o3 的原型链上
继承
回到这个图
对于原型、原型对象和原型链已经没有那么陌生了吧,现在聊聊继承,继承是为了代码复用性,JS的继承方式是实现继承——继承实际的方法,这主要通过原型链实现,基本思想就是通过原型继承多个引用类型的方法和属性。
原型链继承
// 原型链继承
function Person() {
this.name = ['Bill', 'Bob']
}
Person.prototype.getName = function() {
return this.name
}
function Student() {}
Student.prototype = new Person()
let s1 = new Student()
s1.name.push('Jay')
console.log(s1.name);//[ 'Bill', 'Bob', 'Jay' ]
let s2 = new Student()
console.log(s2.name) //[ 'Bill', 'Bob', 'Jay' ]
显而易见,原型链继承不能直接使用,它会出现两个问题:
原型中包含的引用值会在所有实例中共享,所以上述代码中在实例对象s1对name属性进行修改时也会反映在s2中。
子类型实例化时不能给父类型的构造函数传参。
盗用构造函数继承
// 盗用构造函数继承
function Person() {
this.name = ['Bill', 'Bob']
}
function Student() {
// 继承 Person
// 等于在Student上运行Person的所有初始化代码,让每个实例有自己的name属性
Person.call(this);
}
let s1 = new Student()
s1.name.push('Jay')
console.log(s1.name);//[ 'Bill', 'Bob', 'Jay' ]
let s2 = new Student()
console.log(s2.name) //[ 'Bill', 'Bob', ]
Person.call(this) 等于在Student上运行Person的所有初始化代码,让每个实例有自己的name属性。所有盗用构造函数就解决了原型链中的引用类型共享问题,那子类构造函数向父类构造函数传参问题呢?也能解决!
function Person(name) {
this.name = name
}
function Student() {
// 继承 Person 传参
Person.call(this,'Bob');
// 实例属性
this.age = 18
}
let s1 = new Student()
console.log(s1.name) //Bob
console.log(s1.age); //18
Student构造函数中调用Person构造函数时传入name参数,会在Student的实例上定义name属性。
function Person(name) {
this.name = name
}
function Student(name) {
Person.call(this, name)
}
let s1 = new Student('Jay')
console.log(s1.name);//Jay
let s2 = new Student('Bob')
console.log(s2.name) //Bob
完整版就是这样啦!它解决了原型链中的引用类型共享问题和子类构造函数不能向父类构造函数传参问题。但是也有缺点:
没有共享属性
由于必须在构造函数中定义方法,导致无法重用
通过这两种继承方式可以发现,原型链继承过度重用,而盗用构造函数继承则无法共享,那为何不结合一下呢?于是便有了组合继承。
组合继承
// 组合继承
function Person(name) {
this.name = name
this.friends = ['Bob', 'Bill']
}
Person.prototype.getName = function() {
return this.name
}
function Student(name, age) {
// 继承属性
Person.call(this, name)
this.age = age
}
// 继承方法
Student.prototype = new Person()
Student.prototype.sayAge = function() {
return this.age;
}
let student1 = new Student('Jay', 18)
student1.friends.push('Amy')
console.log(student1.friends); // [ 'Bob', 'Bill', 'Amy' ]
console.log(student1.getName());// Jay
console.log(student1.sayAge());// 18
let student2 = new Student('Van', 22)
console.log(student2.friends) // [ 'Bob', 'Bill' ]
console.log(student2.getName());// Van
console.log(student2.sayAge());// 22
组合继承的基本思路就是使用原型链继承原型的属性和方法,通过构造函数继承实例属性。上述代码中,Student构造函数调用Person构造函数,并传入name参数,并把Student.prototype赋值为Person的实例,从而使创建的 student1和student2实例具备自己的属性,同时共享相同的方法。看起来组合继承已经很完美了,其实不然,它存在效率问题:
- 父类构造函数会被调用两次,一次在于子类原型时调用,另一次在子类构造函数中调用。
寄生式组合继承
为了解决组合继承的问题,采用了寄生式组合继承,基本思路为不通过父类构造函数给子类原型赋值Student.prototype = new Person(),而是通过一个函数来获取父类原型的副本,从而只调用了一次父类构造函数。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
// 寄生式组合继承核心逻辑
function inheritPrototype(child,parent){
// 创建父类原型的副本
let prototypeParent = object(parent.prototype);
//把新建立的 原型对象 赋值constructor
prototypeParent.constructor = child;
// 赋值给 子类原型
child.prototype = prototypeParent;
}
function Person(name) {
this.name = name
this.friends = ['Bob', 'Bill']
}
Person.prototype.getName = function() {
return this.name
}
function Student(name, age) {
// 继承属性
Person.call(this, name)
this.age = age
}
// 组合继承方法
// Student.prototype = new Person()
// 寄生式组合继承
inheritPrototype(Student,Person);
Student.prototype.sayAge = function() {
return this.age;
}
let student1 = new Student('Jay', 18)
student1.friends.push('Amy')
console.log(student1.friends); // [ 'Bob', 'Bill', 'Amy' ]
console.log(student1.getName());// Jay
console.log(student1.sayAge());// 18
let student2 = new Student('Van', 22)
console.log(student2.friends) // [ 'Bob', 'Bill' ]
console.log(student2.getName());// Van
console.log(student2.sayAge());// 22
寄生式组合继承可以算是引用类型继承的最佳模式了,但是代码还是太多了,有没有更简洁的继承实现呢? 于是ES6引进了class关键字,实际上class只是语法糖,背后实现的仍然是原型和构造函数。
类继承
class Person {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Student extends Person {
constructor(name, age) {
super(name)
this.age = age
}
}
let p1 = new Person('Bob')
console.log(p1.name); // Bob
console.log(p1.getName());//Bob
let s1 = new Student('Jay',18)
console.log(s1.name,s1.age); // Jay 18
console.log(s1.getName());//Jay
总结
文章就写到这里了,从原型、原型链到new和instanceof的实现,再到继承,都是很基础的内容,这只是我在学习中对知识的总结和理解,毕竟知识输出才是最好的学习方式,不过在下作为前端小白,对于这方面的知识可能还有理解不到位的地方,希望各位多多担待并辛苦在下方评论区指出!
参考资料:
《JavaScript高级程序设计》第四版第八章
《你不知道的JavaScript》上卷第四章、第五章