文章目录
原型链基本概念
基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法。
原型链:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,让原型对象等于另一个类型的实例,层层递进,直到指向Object对象为止,就构成了实例与原型的链条。(Object是原型链的顶端。Object.prototype._proto_为null )
- 原型:原型是一个prototype对象,用于表示类型之间的关系。(为每个实例对象存储共享的方法和属性)
- 构造函数
- 实例
原型、构造函数、实例之间的关系:
function Person () {}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.constructor === Person); // true
上面代码person作为Person的实例对象,Person作为Object的实例对象
函数对象Function与Object
每个对象都会有一个原型,就是[[prototype]],在ES规范里该属性是隐藏的,但在浏览器中则以__proto__的形式暴露出来
- Object是构造函数,且所有的构造函数都是Function的实例,所以
Object.__proto__ === Function.prototype
- 由于Function本身也是一个函数,所以
Function.__proto__ === Function.prototype
- Function.prototype是一个对象,所以
Function.prototype.__proto__ === Object.prototype
- Object.prototype.__proto__是顶级对象,所以
Object.prototype.__proto__ === null
_proto_和prototype的区别
_proto_:
指向原型对象
prototype:
指向一个有constructor
属性的原型对象。
实现继承,es5和es6
1. 原型链继承
- 优点:
- 简单易实现
- 父类新增原型方法/原型属性,子类都能访问
- 实例是子类的实例也是父类的实例
- 缺点:
- 为子类新增属性和方法,不能在构造函数中
- 无法实现多继承
- 创建子类实例时,不能向父类构造函数传参数
- 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型上引用类型的属性,另一个实例的原型属性也会被修改!)
function Person(name) {
this.name = name
this.say = function () {}
}
Person.prototype.listen = function () {}
function Student() { }
Student.prototype = new Person() //关键
2. 借用构造函数继承
- 优点:
- 解决了原型链继承中构造函数引用类型共享的问题
- 可以向构造函数传参(通过call传参)
- 缺点:
- 父类的原型方法不会被子类继承
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Person(name) {
this.name = name
this.say = function () {}
}
Person.prototype.listen = function () {}
function Student() {
Person.call(this) // 关键
}
let st1 = new Student();
st1.listen() //报错,listen is undefined
3.组合继承(原型链继承 + 构造函数继承)
- 缺点:
- 父类的构造函数执行了两遍:一次在子类的构造函数中call方法执行一遍,一次在子类原型实例化父类的时候执行一遍。
function Person(name) {
this.name = name
this.say = function () {}
}
Person.prototype.listen = function () {}
function Student() {
Person.call(this) // 关键
}
Student.prototype = new Person()
4.原型式继承(es5 Object.create)
- 缺点:包含引用类型的属性值始终都会共享相应的值
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
var person = {
name: "person",
friends: ["a", "b", "c"]
};
let person1 = object(person);
person1.name = "person1";
person1.friends.push("d");
let person2 = object(person);
person2.name = "person2";
person2.friends.push("e");
console.log(person1.name) // person1
console.log(person1.friends) // [a,b,c,d,e]
5. 寄生式继承
- 优点:函数的主要作用是为构造函数新增属性和方法,以增强函数
- 缺点:包含引用类型的属性值始终都会共享相应的值
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
//寄生式继承
function changeData(obj) {
const newData = object(obj);
newData.age = 23;
return newData;
}
let person = {
name: "person",
friends: ["a", "b", "c"]
};
let person1 = changeData(person);
6. 寄生组合式继承(借用寄生 + 组合继承),最常用最优的继承方式
- 优点:解决了组合继承两次调用父类构造函数的问题
// 原型式继承
function object(obj){
function F(){}
F.prototype = obj;
return new F();
}
function inherit(child, parent){ // 核心逻辑
let prototype = object(parent.prototype)
prototype.constructor = child;
child.prototype = prototype; // 核心,这里解决了两次调用父类构造(这里没有调用)
}
function Person(name) {
this.name = 'person name'
this.say = function () {
console.log('person say')
}
}
Person.prototype.listen = function () {
console.log('person.prototype listen')
}
function Student() {
Person.call(this) // 构造函数继承
}
inherit(Student, Person)
let stu1 = new Student();
console.log(stu1, stu1.name) // Student {name: 'person name', say: ƒ} 'person name'
stu1.say() // person say
stu1.listen() // person.prototype listen
7. es6 extends
- 子类必须在constructor()方法中调用super(),否则就会报错(这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象)
- 可以不显式的写constructor(),如果不写就会隐性添加,然后调用super()
- 新建子类实例时,父类的构造函数必定会先运行一次
- 父类的私有属性和私有方法无法被子类继承(#定义的,比如#p=1,p属性无法被继承)
- 静态属性和静态方法的继承(static定义的),静态属性是浅拷贝
Object.getPrototypeOf(a) === Person
class Person {
constructor() {
console.log('Person constructor');
}
}
class Student extends Person {
constructor() {
super(); // 调用父类的constructor
console.log('Student constructor');
}
}
let a = new Student()
// 会依次输出 Person constructor
// Student constructor
8. es6 extends与es5 的继承方式有什么区别?
- ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
- ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
instanceOf是如何进行判断对象类型的
1. instanceOf概念
用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
通过判断实例的_proto_
属性与构造函数的prototype
属性是否指向同一个原型对象。
注意1: 实例的_proto_
指向的是构造函数的prototype
属性,与构造函数没有关系。
注意2: 原型上面可能还有会有原型,会沿着原型链继续向上找,找到返回true,反之返回false。
2. 原生实现
function myInstanceOf(left,right){
if (typeof left !== 'object' || left === null) return false;
let prototype = right.prototype; // 拿到right的原型
// 或者可以用Object.getPrototypeOf(left)
let proto = left.__proto__; // left作为实例,拿到left的原型
while(true){
if(proto === prototype) return true;
if(proto === null) return false;
//若本次查找无结果,则沿着原型链向上查找
proto = proto.__proto__;
}
}
let a = [1,2,3];
console.log(myInstanceOf(a,Array));
//true
- 举一个栗子:
foo instanceof Foo //结果为true,说明foo._proto_ == Foo.prototype;
但是我们不能轻易说明foo
是Object
的实例,只有以下满足,才是Object
的实例。
foo instanceof Object //结果为true,说明Foo.prototype._proto_ == Object.prototype
3. 想一想:如果A继承B,B继承C,C继承D,那么怎么判断a是由A直接生成的实例还是B直接生成的?
a._proto_.constructor == B
通过constructor
来判断,a由B直接生成
4. instanceof能否判断基本数据类型?
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
new运算符
- 创建一个空对象obj,并让其继承func.prototype
- 执行构造函数,并将this指向创建的空对象obj
- 返回创建的对象obj,new一个实例的时候,如果没有return,就会根据构造函数内部this绑定的值生成对象,如果有返回值,就会根据返回值生成对象,为了模拟这一效果,就需要判断apply后是否有返回值
1.原生实现new操作
function myNew(func, ...args){
//1.创建一个空对象obj,并让其继承func.prototype
//等同于let obj={};obj._proto_ = func.prototype;
let obj = Object.create(func.prototype);
//2.执行构造函数,并将this指向创建的空对象obj
let result = func.apply(obj,arguments);
//3.返回创建的对象obj,new一个实例的时候
//如果没有return,就会根据构造函数内部this绑定的值生成对象,
//如果有返回值,就会根据返回值生成对象,为了模拟这一效果,就需要判断apply后是否有返回值
return obj instanceof Object ? result : obj;
}
console.log(myNew(Person, 'HANMEI'));
Object.create(),实现原理和继承,与 new Object 的区别
1. 基本用法
Object.create(proto, propertiesObject)
- proto 新创建对象的原型对象
- propertiesObject,属性对应于 Object.defineProperties() 的第二个参数
// 创建一个以o1为原型,增加b属性的对象 o2 let o1 = {a: 'a'} let o2 = Object.create( o1, { b: { value: 'b', writable: true, enumerable: true, configurable: true, }, }, );
2. 类式继承
3. 与new Object 的区别
- 创建对象的方式不同
new Object()
通过构造函数来创建对象, 添加的属性是在自身实例下Object.create()
是 ES6 创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下,传入的第一个参数是直接作为新对象的原型对象的,无法通过 instanceof 运算符来判断其类型
- 创建空对象时不同
- 当用构造函数或对象字面量方法创建空对象时,对象时有原型属性的,即有
__proto__
; - 当用 Object.create() 方法创建空对象时,对象是没有原型属性的。
- 当用构造函数或对象字面量方法创建空对象时,对象时有原型属性的,即有
// new Object() 方式创建
let a = { rep : 'apple' }
let b = new Object(a)
console.log(b) // {rep: "apple"}
console.log(b.__proto__) // {}
console.log(b.rep) // apple
// Object.create() 方式创建
let c = Object.create(a)
console.log(c) // {}
console.log(c.__proto__) // {rep: "apple"}
console.log(c.rep) // apple
4. 实现原理
Object.cteate = function(proto) {
function F() {}
F.prototype = proto
return new F()
}
面试题
1.代码输出题(构造函数和实例对象的原型链问题)
Function.prototype.a = () => {
console.log(1);
}
Object.prototype.b = () => {
console.log(2);
}
function A() {}
const a = new A();
a.a(); // 报错
a.b(); // 2
A.a(); // 1
A.b(); // 2
解析:对于a,作为A的实例对象,会沿着原型链向上查找,查找顺序为:
- a
a.__proto__
也就是A.prototypeA.prototype.__proto__
也就是Object.prototypeObject.prototype.__proto__
也就是null,因为Object.prototype.__proto__ === null
对于A
- A
A.__proto__
也就是Function.prototypeFunction.prototype.__proto__
也就是Object.prototypeObject.prototype.__proto__
也就是null
2. 代码输出题(原型链上的属性共享问题)
function A() {
}
A.prototype.n = 0;
A.prototype.add = function () {
this.n += 1;
}
let a = new A();
let b = new A();
a.add();
console.log(b.n) // 0
解析:new之后的实例的this指向的是实例,所以当a执行add()的时候,相当于给a自身加一个属性为n,值为1,原型链上的n还是0,b自身没有n属性,所以去原型上找,得到0
3.代码输出题(两个实例之间的属性共享问题)
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(age + "岁的" + name + "在吃饭。");
}
}
Person.run = function () {}
Person.prototype.walk = function () {}
let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);
console.log(p1.eat === p2.eat); // false,
console.log(p1.name === p2.name); // true,
console.log(p1.run === p2.run); // true,
console.log(p1.walk === p2.walk); // true
解析:
- new之后,相当于把所有属性都重新拷贝了一份给实例,对于引用对象来说,新开辟的空间地址肯定不相同
- 简单类型进行比较,‘jsliang’ === ‘jsliang’
- run方法只是作为Person自己的静态属性,不给实例共享,所以为undefined === undefined
- 原型上的方法是所有实例共享的
4.按照如下要求实现Person 和 Student 对象
- Student 继承Person
- Person 包含一个实例变量 name, 包含一个方法 printName
- Student 包含一个实例变量 score, 包含一个实例方法printScore
- 所有Person和Student对象之间共享一个方法
// 原生
function Person(name) {
this.name = name;
this.printName = function() {
}
}
Person.prototype.commonMethods = function() {
}
function Student(score) {
Person.call(this,score);
this.score = score;
this.printScore = function() {
}
}
Student.prototype = new Person();
let a = new Student('11')
let b = new Student('22')
console.log(a.commonMethods === b.commonMethods) // true
// es6
class Person {
constructor(name) {
this.name = name;
}
printName() {}
commonMethods(){}
}
class Student extends Person {
constructor(name, score) {
super(name);
this.score = score;
}
printScore() {}
}
let stu = new Student('小红');
let person = new Person('小紫');
console.log(stu.commonMethods === person.commonMethods); //true
5. 代码输出题(原型和实例方法共享问题)
function Parent(){
this.a = 'Parent'
}
function Tom() {
this.a = 'Tom'
}
Parent.__proto__.print = function(){ // Parent.__proto__ === Function.prototype
console.log(this.a)
}
Parent.print() // undefined
Tom.print() // undefined, print方法添加到了Function的原型上,所以可以访问到
var child = new Parent()
child.print() // 报错 ,Parent.prototype.__proto__ === Object.prototype
// Parent构造函数没有print方法,构造函数的原型上也没有,原型的原型上也没有,找到了终点都没找到,所以报错
6. 代码输出题(构造函数和原型对象方法共享问题, Object和Function)
function Test() {} // Test是通过Function实例化出来的
Object.prototype.printName = function() {
console.log('Object');
}
Function.prototype.printName = function() {
console.log('Function');
}
Test.printName(); // Function
var obj = new Test();
obj.printName(); // Object
// Test构造函数上没有这个方法,会去Test原型对象上找,原型对象也没有,原型对象的原型是Object.prototype