深入理解JS原型,原型链,继承及new的实现原理

2,671 阅读4分钟

prototype(原型对象)

只有函数有这个属性

let obj={}
function fun(){}
console.log(obj.prototype)      //undifined
console.log(fun.prototype)      //{constructor: f,__proto__: Object}

为什么只有函数有这个属性

js通过new来生成构造函数,但构造函数因为传参不同生成的对象都不一样,有时候生成的多个实例对象需要一种共享属性。但是js设计之初又没有类的概念,无法实现。于是就有了prototype,用这个对象来存所有实例化对象的共享属性。

举个栗子

function Person(name,age){
    this.name=name
    this.getAge=function(){
        console.log(`我${age}岁了`)
    }
}
Person.prototype.type='人'
Person.prototype.say=function(){
    console.log('hello world')
}
let person1=new Person('personOne',10)
let person2=new Person('personTwo',20)
console.log(person1.name)               //personOne
console.log(person2.name)               //personTwo
person1.getAge()                        //我10岁了
person2.getAge()                        //我20岁了
console.log(person1.type)               //人
console.log(person2.type)               //人
person1.say()                           //hello world
person2.say()                           //hello world

person1person都是同一个构造函数构造出来的对象,因为传参不同他们的名字也不一样,年龄也不一样。但是实例化多少个对象,他们直接调用say方法永远输出的都是hello world,type属性也永远是人。这就是共享属性


继承

维基百科:继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。

通俗点讲就是把父类中的属性和方法拷贝了一份放到了子类中。

继承实现

function Person(name) {
    this.name = name
    this.getName=function(){
        console.log(`my name is ${name}`)
    }
}
Person.prototype.say = function () {
    console.log('hello world')
}

function My() {
    Person.apply(this, arguments)                 //My继承Person中的属性和方法
}
My.prototype = Object.create(Person.prototype)   //My继承Person原型对象中属性
My.prototype.constructor = My                    //修正原型对象构造函数指向
let my = new My('hkj')      
console.log(my.name)                             //hkj
my.getName()                                     //my name is hkj
my.say()                                         //hello world

tip:在本文中prototype,原型对象,共享属性均表示同一个东西

可以看到My中并没有定义name值,getNamesay方法,但是实例化出来的my依然可以调用,这就是继承了Person中的属性和方法。而上面代码中最核心的部分就是这两行,①Person.apply(this, arguments) 当前对象继承Person中属性和方法,②My.prototype = Object.create(Person.prototype) My的共享属性prototype也继承了Person的共享属性prototype


原型链

可以看到上面原型继承中我用了一行My.prototype = Object.create(Person.prototype)这个代码,其实这是Object中的一个用于继承prototype的方法。如果换种写法,上面代码全等于:My.prototype.__proto__ = Person.prototype

__proto__是什么

这个属性其实就是共享属性继承的一座桥,可以理解为它将Person共享属性拷贝到My的共享属性中,你可能会想为什么不直接用My.prototype = Person.prototype这样不就也可以继承属性了吗。但是这种赋值方式会导致MyprototypePersonprototype指向同一个对象,当Myprototype进行操作时也会影响PersonPrototype。而有了__proto__以后就相当于拷贝了一个副本到My中,两者的prototype相互独立互不影响。

function Person(name) {
    this.name = name
    this.getName=function(){
        console.log(`my name is ${name}`)
    }
}
Person.prototype.say = function () {
    console.log('hello world')
}

function My() {
    Person.apply(this, arguments)   //My继承Person的属性和方法
}
My.prototype = Object.create(Person.prototype) //My继承Person原型对象中属性
My.prototype.constructor = My 
let my = new My('hkj')
console.log(my.__proto__)                   // 1
console.log(My.prototype)                   // 2
console.log(My.prototype.__proto__)         // 3
console.log(Person.prototype)               // 4
console.log(Person.prototype.__proto__)     // 5
console.log(Object.prototype)               // 6

分析输出结果,发现1和2,3和4,5和6都指向的是同一个对象。套用刚才的知识点my继承了My中的共享属性所以my.__proto__My.prototype相等。而Myprototype继承了Personprototype中的属性所以My.prototype.__proto__Person.prototype相等。可以看到通过__proto__这个属性将实例my、子类My与父类Person的共享属性像链条一样连接了起来,这就是原型链

Object.prototype

可以看到最后有两行代码也相等Person.prototype.__proto__Object.prototype。这是因为所有对象都继承了Object的方法。Object.prototype为所有原型链的终点。对象原型继承了Object原型中的属性和方法。因此Person.prototype.__proto__Object.prototype相等。 因为Object已经到了最顶级,所以Object.prototype.__proto__指向的对象为null,它不继承于任何对象。

比如在平常开发过程中我们经常用到了toString方法,但在对象中并未定义但依然可以调用,这就是因为它是Object.prototype中定义的方法,所有对象就算没有定义依然可调用。


new

首先解释一下new干了什么: 用new实例化一个对象后,该对象会继承构造函数及其原型上的属性。可以发现其实new的操作也是一种继承,不过不是类与类之间的继承,而是实例与类之间的继承。

实现步骤

  • 生成一个中间对象
  • 使该对象继承构造函数属性和方法
  • 使该对象继承构造函数prototype上的属性和方法
  • 返回该对象
function _new() {
    let obj = {} // 生成一个空对象
    let Constructor = [].shift.call(arguments)              // 获取第一个参数(构造函数)
    obj.__proto__ = Constructor.prototype                   // 对象继承构造函数原型属性方法
    let result = Constructor.apply(obj, arguments)          // 对象this指向构造函数(继承构造函数的属性和方法)
    return typeof result === 'object' ? result : obj        // 确保返回的是一个对象
}

function Person(name) {
    this.name = name;
    this.say = function () {
        console.log('my name is ' + name);
    }
}
Person.prototype.type = '人'
let person = new Person('hkj')
let person1 = _new(Person,'hkj')
console.log(person)
console.log(person1)

可以发现new的实现方式与上面的类的继承极其相似,这也就解释了上面my.__proto__My.prototype相等的原因。


小结

原型作为JS核心知识点之一,也是面试高频考点。因为其抽象难懂的特性,初学者对这个概念往往云里雾里,有些老鸟也不一定能完全理解,当年的我也被这个知识点折磨了许久。但作为js最重要的特性之一,想要提升技术还是得啃下这块硬骨头。如果对call和apply用法不太熟练的同学可以查看下一篇文章:深入理解JS中call及apply的用法

写作不易,各位小哥哥小姐姐觉得我的博客对你有帮助的话,请给我点个赞哟!