首页IT科技js原型链面试题怎么回答(深入原型链与继承(详解JS继承原理))

js原型链面试题怎么回答(深入原型链与继承(详解JS继承原理))

时间2025-06-20 19:46:46分类IT科技浏览5329
导读:原型链与继承 new 关键字的执行过程...

原型链与继承

new 关键字的执行过程

让我们回顾一下              ,this 指向里提到的new关键字执行过程              。

创建一个新的空对象 将构造函数的原型赋给新创建对象(实例)的隐式原型 利用显式绑定将构造函数的 this 绑定到新创建对象并为其添加属性 返回这个对象

手写new关键字的执行过程:

function myNew(fn, ...args) { // 构造函数作为参数 let obj = {} obj.__proto__ = fn.prototype fn.apply(obj, args) return obj }

这里提到了__proto__ 和prototype:前者被称为隐式原型                     ,后者被称为显式原型                      。

构造函数              、实例对象和原型对象

三者的概念

构造函数:用于生成实例对象       。构造函数可分为两类:

自定义构造函数:function foo () {} 原生构造函数:function Function () {}和function Object () {}等

原型对象:每个构造函数都有自己的原型对象        ,可通过prototype访问              。

实例对象:可由构造函数通过new关键字生成的对象                      。

三者的关系

构造函数可以通过prototype访问其原型对象       ,而原型对象可通过constructor访问其构造函数       。构造函数可通过new关键字创建实例对象                     ,实例对象可通过__proto__ 访问其原型对象       。

我们来看一段代码的输出结果:

function Foo(name, age) { this.name = name this.age = age } let a = new Foo(小明, 22) console.log(构造函数:, Foo) console.log(原型对象, Foo.prototype) console.log(实例对象, a) // 可以输出一下               ,看看它们都是什么样子

可以看出实例对象内部的第一个[[Prototype]]的展开内容等于原型对象的展开内容       ,可构建一个等式如下:

// 实例对象可通过 __proto__ 访问其原型对象 a.__proto__ === Foo.prototype // true // 原型对象可通过 constructor 访问其构造函数 Foo.prototype.constructor === Foo // true

原型链的概念及图解

来看一张关于原型链的经典图:

上面这张图的箭头乍一看能让人头疼                     ,我们对图中的元素进行分类并划分层次               ,可有以下三层:

第一层__proto__指向:实例对象

通过构造函数生成的实例对象 // 生成实例对象 function Foo() {} let obj1 = new Foo() // __proto__指向验证 obj1.__proto__ === Foo.prototype // true 通过new Object()                      、对象字面量生成的实例对象 // 生成实例对象 let obj2 = new Object() // __proto__指向验证 obj2.__proto__ === Object.prototype // true 通过function或class声明生成的实例对象 // 生成实例对象 function Foo(){} // 原生构造函数 // function Function(){} // function Object(){} // __proto__指向验证 Foo.__proto__ === Function.prototype // true Function.__proto__ === Function.prototype // true Object.__proto__ === Function.prototype // true

说明:其实我们自己定义的函数也是由Function构造函数生成的实例对象                      。

第二层__proto__指向:Function.prototype和Foo.prototype

Foo.prototype.__proto__ === Object.prototype // true Function.prototype.__proto__ === Object.prototype // true

第三层__proto__指向:Object.prototype)

Object.prototype.__proto__ === null // true

我们自己再画一张图看一下:

自底向上有三层的__proto__构成基本的JavaScript原型模式生态,最后再总结一下规则:

实例对象都会指向其构造函数原型 构造函数原型都会指向Object.prototype Object.prototype最终指向null

总结:其实我们的原型链指的就是__proto__的路径              。

注意:这里只是为了原型链能更加直观                     ,请不要忘了构造函数原型的constructor属性                      ,它会指回对应的构造函数       。

原型链继承

我们利用任务驱动型的方法去学习继承方式,考虑这样一个类结构:

普通用户:作为父类 VIP 用户:作为子类

说明:VIP用户需要继承普通用户                      。其中              ,VIP用户的武器列表可以添加屠龙宝刀              。

原型链搜索机制:若要访问当前对象所没有的属性和方法                      ,则会首先以当前对象为起点沿着原型链__proto__向上寻找每个对象内部的属性和方法。直到找到对应的属性和方法        ,没有则会直接走到原型链尽头null                      。

来看这样一段代码:

function USER(username, password) { this.username = username this.password = password this.weapon = [水果小刀] } VIP.prototype = new USER() // 为什么要放到中间? // 注意:改写原型              ,要记得把 constructor 指会原构造函数 VIP.prototype.constructor = VIP function VIP() { } VIP.prototype.addWeapon = function (weaponName) { this.weapon.push(weaponName) } let a = new VIP(小明) // 缺陷1:无法给父类构造函数传参                     ,只能在 VIP 中自行添加相应参数                      。无法实现父类属性重用 let b = new VIP() b.addWeapon(屠龙宝刀) console.log(b.weapon) let c = new VIP() console.log(c.weapon) // 缺陷2: 我们想要单独给实例 b 的武器列表添加一把屠龙宝刀        ,结果是实例 c 的武器列表也会增加屠龙宝刀

原型链__proto__实现继承会经过的对象(从子类实例到父类原型):

子类构造函数实例 :new VIP()

父类构造函数实例 :new USER()

父类构造函数原型 :USER.prototype

我们可以构建两个表达式去验证:

new VIP().__proto__ === new USER() // true new USER().__proto__ === USER.prototype // true

再进一步提炼以上两个表达式       ,可获得最终表达式。以下为实现继承关键的完整原型链:

// VIP 构造函数所生成的实例会经过两层__proto__找到父类原型 new VIP().__proto__.__proto__ === USER.prototype // 接下来                     ,由 VIP 构造函数生成的实例所没有的属性和方法               ,都会去父类原型找到属性和方法              。

原型链继承缺点:

父类原型中若存在的引用值则会在所有实例间共享                      。 子类构造函数在实例化时不能给父类构造函数传参       ,即我们的父类属性无法重用       。

为什么 VIP.prototype = new USER() 这一步要放到两个构造函数中间?

如果这一步表达式放到后面                     ,我们的VIP.prototype是其原本构造函数 VIP 的原型              。在这个原本的构造函数原型上添加方法               ,不会有继承效果                      。

我们的想法是通过父类构造函数生成实例,利用它实例的__proto__去实现继承效果       。要想在子类构造函数添加方法                     ,我们实际做了这样的操作       。如下:

// new USER()就是我们父类构造函数生成的实例 new USER().addWeapon = function (weaponName) { this.weapon.push(weaponName) }

但是上面这样会出现问题                      ,我们子类构造函数怎么办?他想new一个实例,还是会根据原来的原型                      。

因此              ,我们需要将new USER()传递给VIP.prototype              。这样VIP构造函数生成实例才会有继承效果                      ,如下:

VIP.prototype = new USER() // 传递__proto__实现继承 VIP.prototype.addWeapon = function (weaponName) { this.weapon.push(weaponName) }

盗用构造函数

来看这样一段代码:

function USER(username, password) { this.username = username this.password = password this.weapon = [水果小刀] } function VIP(username, password) { USER.call(this, username, password) // 调用父类构造函数        ,为其属性赋值 } // 这里的 this 指向子类构造函数生成的新实例 // 1. 接下来我们可以向父类构造函数传参 let a = new VIP(小红) console.log(a.username) // 小红 // 2. 也可以解决引用值产生的问题 let b = new VIP() b.weapon.push(屠龙宝刀) console.log(b.weapon) // [水果小刀, 屠龙宝刀] let c = new VIP() console.log(c.weapon) // [水果小刀] // 这样实例 b 和 c 的武器列表的数据都是独立的

过程解析:new VIP(小红)传入了一个“小红              ”参数       。

第一次绑定操作:new执行过程会执行一次绑定操作              ,将this指向实例对象                      。

第二次绑定操作:VIP构造函数内部的call方法再次绑定实例对象                     ,调用父类构造函数

总结:我们通过传参实际调用了两次绑定操作        ,最终使得子类构造函数的新实例也能拥有父类的属性和值              。

盗用构造函数缺点:

只能在构造函数内部定义方法使用       ,不能访问父类原型定义的方法。即我们的父类方法不能重用

组合继承( = 原型链继承 + 盗用构造函数 )

如果你已经清楚的知道上面两种继承方式的优点和缺陷                     ,那么我们可以利用1 + 1 > 2 的方法实现组合继承                      。

function USER(username, password) { this.username = username this.password = password this.weapon = [水果小刀] } VIP.prototype = new USER() // 注意:改写原型               ,要记得把 constructor 指会原构造函数 VIP.prototype.constructor = VIP function VIP(username, password) { USER.call(this, username, password) // 调用父类构造函数       ,为其属性赋值 } // 这里的 this 指向子类构造函数生成的新实例 VIP.prototype.addWeapon = function (weaponName) { this.weapon.push(weaponName) } // 我们尝试给父类构造函数传参 let a = new VIP(小红) console.log(a.username) // 小红 // 看看添加屠龙宝刀                     ,有没有相互影响 let b = new VIP() b.addWeapon(屠龙宝刀) console.log(b.weapon) let c = new VIP() console.log(c.weapon)

以上的组合继承方式输出了正确的答案               ,算是完美解决了原型链继承和盗用构造函数继承出现的问题                      。我们将以上代码放入浏览器打断点分析。如下:

很明显,我们第一次new USER()会调用父类构造函数                     ,而后子类构造函数每一次生成新实例都会调用父类构造函数              。也就是说                      ,多了第一次会调用父类构造函数的情况                      。

原型继承

在 JavaScirpt 高级程序设计 8.3.4 中提到了这种方式,来看这样一段代码

function object(obj) { function Fn() { } Fn.prototype = obj return new Fn() // 返回一个空函数              ,其内部原型改写为 obj }

有没有熟悉的感觉                      ,其实正是我们之前手写bind函数利用的继承方法       。与ES6中的Object.create()方法效果相同              。它适于在原有对象的基础上再克隆一个对象                      。此外        ,对象属性值若为原始值则可以进行改写              ,若为引用值则会产生引用值的特点       。即多个克隆对象会共享同一个引用值                     ,也就是说这个“克隆                      ”操作相当于我们的浅拷贝操作       。

寄生继承

function createAnother(obj) { let clone = object(obj) clone.sayHello = () => { console.log(Hello World) } }

这种方式可以使克隆对象在原基础上增强        ,即添加属性和方法       ,

注意:原型继承和寄生继承都重点关注对象的使用                     ,而不考虑构造函数的使用

寄生组合继承( = 组合继承 + 原型继承 + 寄生继承 )

我们可以再利用浏览器打断点试试               ,是不是不会发生像组合继承那样首次调用构造函数的情况                      。

// 寄生组合继承 function inheritPrototype(subType, superType) { subType.prototype = Object.create(superType.prototype) // 创建对象 subType.prototype.constructor = subType // 增强对象 } function USER(username, password) { this.username = username this.password = password this.weapon = [水果小刀] } // 验证表达式时       ,下面这一条语句要加上注释              。 inheritPrototype(VIP, USER) // 调用继承函数                     , // 验证表达式时               ,把下面这两条语句注释去掉       。 // VIP.prototype = Object.create(USER.prototype) // 创建对象 // VIP.prototype.constructor = VIP // 增强对象 function VIP(username, password) { USER.call(this, username, password) // 调用父类构造函数,为其属性赋值 } // 这里的 this 指向子类构造函数生成的新实例 VIP.prototype.addWeapon = function (weaponName) { this.weapon.push(weaponName) } // 我们尝试给父类构造函数传参 let a = new VIP(小红) console.log(a.username) // 小红 // 看看添加屠龙宝刀                     ,有没有相互影响 let b = new VIP() b.addWeapon(屠龙宝刀) console.log(b.weapon) let c = new VIP() console.log(c.weapon)

原型链__proto__实现继承会经过的对象(从子类实例到父类原型):

子类构造函数实例 :new VIP() 空构造函数实例 :Object.create(USER.prototype) 父类构造函数原型 :USER.prototype

我们同样构建两个表达式去验证:

new VIP().__proto__ === Object.create(USER.prototype) // true Object.create(USER.prototype).__proto__ === USER.prototype // true

再进一步提炼以上两个表达式                      ,可获得最终表达式                      。以下为实现继承关键的完整原型链:

new VIP().__proto__.__proto__ === USER.prototype // true // 接下来,由 VIP 构造函数生成的实例所没有的属性和方法              ,都会去父类原型找到属性和方法              。

原型链和寄生组合的继承区别比较

原型链的继承实现:利用new USER()作为跳板实现继承。

VIP.prototype = new USER() // 传递__proto__实现继承 new VIP().__proto__ === new USER() // true new USER().__proto__ === USER.prototype // true

寄生组合的继承实现:利用Object.create(USER.prototype)作为跳板实现继承                      。

VIP.prototype = Object.create(USER.prototype) // 传递__proto__实现继承 new VIP().__proto__ === Object.create(USER.prototype) // true Object.create(USER.prototype).__proto__ === USER.prototype // true

注意:Object.create(USER.prototype)会返回一个空函数实例                      ,这个实例的__proro__指向USER()构造函数                      。

class继承(ES6 语法)( ≈ 寄生组合继承 )

在 ES5 之前我们都是利用构造函数实现面向对象编程。ES6 的class作为语法糖        ,其实内部也是利用了构造函数实现面向对象编程              。

class USER { constructor(username, password) { this.username = username this.password = password this.weapon = [水果小刀] } } class VIP extends USER { constructor(username, password) { super(username, password) // 调用父类构造函数              ,相当于执行 call 方法 } addWeapon(weaponName) { this.weapon.push(weaponName) } } // 我们尝试给父类构造函数传参 let a = new VIP(小红) console.log(a.username) // 小红 // 看看添加屠龙宝刀                     ,有没有相互影响 let b = new VIP() b.addWeapon(屠龙宝刀) console.log(b.weapon) let c = new VIP() console.log(c.weapon) // 以上同样可运行我们的测试代码

总结 JavaScript 继承方式:

原型链继承 盗用构造函数继承 组合继承( = 原型链继承 + 盗用构造函数 ) 原型式继承 寄生继承 寄生组合继承( = 组合继承 + 原型继承 + 寄生继承 ) class继承( ≈ 寄生组合继承 )

以上可以看出 JavaScript 对与继承方式的优化是一个多次迭代不断优化的过程                      。

参考

JavaScript高级程序设计(第4版)

声明:本站所有文章        ,如无特殊说明或标注       ,均为本站原创发布       。任何个人或组织                     ,在未征得本站同意时               ,禁止复制       、盗用              、采集                      、发布本站内容到任何网站       、书籍等各类媒体平台              。如若本站内容侵犯了原著者的合法权益       ,可联系我们进行处理                      。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
《番茄工作法》(番茄工作法_Feisky_新浪博客)