写在开头
在刷掘金的时候,看到大佬在一篇文章下的评论,好精辟
- 所有的构造函数都是 Function 的实例
- 所有原型对象都是 Object 的实例,除了
Object.prototype
概念
-
构造函数、原型和实例的关系
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针。
-
原型链
在对象向上查找过程中,prototype 在这个规则中充当链接的作用,于是我们把这种实例与原型的链条称为原型链。
四个规则
引用类型的四个规则
-
引用类型,都具有对象特性,即可自由扩展属性。
jsconst obj = {} const arr = [] const fn = function () {} obj.a = 1 arr.a = 1 fn.a = 1 console.log(obj.a) // 1 console.log(arr.a) // 1 console.log(fn.a) // 1 -
引用类型,都有一个隐式原型
__proto__属性,属性值是一个普通的对象jsconst obj = {} const arr = [] const fn = function () {} console.log('obj.__proto__', obj.__proto__) console.log('arr.__proto__', arr.__proto__) console.log('fn.__proto__', fn.__proto__)

-
引用类型,隐式原型
__proto__的属性值指向他的构造函数的显式原型prototype属性值jsconst obj = {} const arr = [] const fn = function () {} console.log(obj.__proto__ === Object.prototype) // true console.log(arr.__proto__ === Array.prototype) // true console.log(fn.__proto__ === Function.prototype) // true -
当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么他会去查询它的隐式原型
__proto__中寻找jsconst obj = { a: 1 } obj.toString // ƒ toString() { [native code] }obj本身没有toString属性,所以会去查询它的隐式原型__proto__中寻找,在Object的prototype找到了,所以obj.toString的值就是toString属性的值。
引用类型:Object、Array、Function、Date、RegExp。
一个特例
js
function Person(name) {
this.name = name
}
let nick = new Person('nick')
console.log(nick.toString) // ƒ toString() { [native code] }
正常来说,nick 是 Person 构造函数生成的实例,而 person 的 prototype 并没有 toString 方法,但是为什么 nick 能获取到 toString 方法呢?
这里就引出了 原型链 的概念了,nick 的实例先从自身出发检讨自己,发现并没有 toString 方法。找不到,就往上找,找 Person 构造函数的 prototype 属性值,还是没有找到。构造函数的 prototype 也是一个对象,对象的构造函数是 Object, 所以就找到了 Object.prototype 下的 toString 方法。

一张图片

原型上的方法
instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。
手写 instanceof
js
// 变量 right 的原型是否存在于 left 的原型链上
function instance_of(left, right) {
// 验证如果为基本数据类型,则直接返回 false
const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
if (baseType.includes(typeof left)) return false
let RightP = right.prototype;
let LeftP = left.__proto__;
while (true) {
if (LeftP === null) {
return false // 代表找到了最顶层
}
if (LeftP === RightP) { // 严格相等
return true
}
LeftP = LeftP.__proto__ // 继续向上查找
}
}
再来看一段代码
js
function Foo(name) {
this.name = name
}
let f = new Foo('nick')
f instanceof Foo // true
f instanceof Object // true
尝试理解一下判断流程
f instanceof Foo:f的隐式原型__proto__和Foo.prototype是同一个对象,所以返回truef instanceof Object:f的隐式原型__proto__和Object.prototype不等,所以继续向上查找,f的隐式原型__proto__指向Foo.prototype,所以继续用Foo.prototype的隐式原型__proto__去对比Object.prototype,所以返回true
hasOwnProperty
hasOwnProperty 方法用于检测一个对象是否含有特定的自身属性(即,不能从原型上继承的属性)。
js
console.log(instance1.hasOwnProperty('name')) // true
isPrototypeOf
isPrototypeOf 方法用于测试该对象是否为指定对象的原型。
js
console.log(Father.prototype.isPrototypeOf(instance1)) // true
原型链的问题
- 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例所共享。
- 在创建子类型时,不能向超类型的构造函数中传递参数。
可以尝试弥补原型链的不足
借用构造函数(经典继承)
在子类型构造函数的内部调用超类型构造函数。
js
function Father() {
this.colors = ['red', 'blue', 'green'];
}
function Son() {
Father.call(this); // 继承了 Father 的属性和方法, 且可以向父类型传递参数
}
let instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]
let instance2 = new Son();
console.log(instance2.colors); // ["red", "blue", "green"]
借用构造函数解决了原型链的两大问题
- 保证了原型链中引用类型的值的独立,不再被所有实例所共享。
- 可以向超类型构造函数中传递参数。
组合继承
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。 既通过在原型上定义方法实现了函数复用,又能保证每个实例都有她自己的属性。
js
function Father(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Father.prototype.sayName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name); // 继承示例属性,第一次调用 Father()
this.age = age;
}
Son.prototype = new Father(); // 继承父类方法,第二次调用 Father()
Son.prototype.sayAge = function () {
console.log(this.age);
};
let instance1 = new Son('nick', 18);
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // nick
instance1.sayAge(); // 18
let instance2 = new Son('jack', 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // jack
instance2.sayAge(); // 20
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,是 JavaScript 中最常用的继承模式。 而且 instanceOf 和 isPrototypeOf 也能用于识别基于组合继承创建的对象。
原型继承
在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
js
function createObject(o) {
function F() {
}
F.prototype = o;
return new F();
}
本质上讲,createObject() 函数就是返回了一个引用传入对象的新对象。这样可能会有共享数据的问题。
在 ECMAScript5 中,通过新增 Object.create() 方法,规范了上面的原型继承。
Object.create() 方法接受两个参数:
- 一个用作新对象原型(prototype)的对象
- (可选) 一个为新对象定义额外属性的对象,这个对象与 Object.defineProperties() 方法的参数对象格式相同。以这种方式指定的任何属性都会覆盖新对象原型对象上的同名属性。
原型继承中,包含引用类型值的属性始终都会共享相应的值
寄生式继承
寄生式继承就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
寄生组合式继承
寄生组合式继承就是为了降低调用父类构造函数的开销而出现的
基本思想:不必为了指定子类型的原型而调用超类型的构造函数
js
function extend(subClass, superClass) {
// 创建了一个新对象,这个对象的原型是父类的 superClass.prototype, 也就是说 `prototype` 现在是一个继承了 superClass.prototype 的新对象。
let prototype = Object.create(superClass.prototype);
// 默认情况下,使用 Object.create() 创建的对象的 constructor 会指向 superClass,因此我们需要将它重新设置回 subClass,保持构造函数引用的正确性。
prototype.constructor = subClass;
// 把 subClass.prototype 设置为构建好的 prototype,这样就完成了继承关系的建立。
subClass.prototype = prototype;
}
new 运算符
js
let obj = {};
obj.__proto__ = F.prototype;
F.call(obj);
- 第一行,创建了一个空对象obj。
- 第二行,我们将这个空对象的__proto__设置为了F.prototype。
- 第三行,我们将F函数对象的this指向了obj,然后再调用F函数。
在 new 操作符的作用下,实际上发生了以下事情:
- 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
- 属性和方法被添加到 this 引用的对象上。
- 新创建的对象由 this 所引用,并且最后隐式的返回 this。