javascript(第三篇)原型、原型链、继承问题,使用 es5、es6实现继承,一网打尽所有面试题

没错这是一道【去哪儿】的面试题目,手写一个 es5 的继承,我又没有回答上来,很惭愧,我就只知道 es5 中可以使用原型链实现继承,但是代码一行也写不出来。

关于 js 的继承,是在面试中除了【 this 指针、命名提升、事件循环】之外的又一个重要的题目,而且很容易忽视。

  1. this 指针
  2. 命名提升
  3. 事件循环

这一部分内容,还是建议看一遍《你不知道的javascript 》上这本书,看完了你就会发现,你确实是不知道。

 一、继承的概念

先明确继承的概念,继承主要是针对类的,重要的事情说三遍,继承指的是类的继承,子类继承父类的属性和方法,这个时候和对象是没有关系的。

注意,写继承是针对类,有了继承才有子类、父类这一说

在 es6 中可以使用 class + extends 关键字事件继承,但是问题是 es5 中没有 class 关键字,所以我们就使用函数来实现!

在 JavaScript 中,继承是一种机制,它允许一个对象获取另一个对象的属性和方法。这意味着一个对象可以使用另一个对象的特性,而不必重新定义这些特性

上面的定义虽然说【它允许一个对象获取另一个对象的属性和方法】但是我们的代码写的主要还是函数,并且使用这个函数来生成对象!!

es5 中的继承方法主要有四种,分别是【原型链继承、构造函数继承、组合式继承、寄生式继承】

这些名字挺能忽悠人的,尤其是最后一个!

别看一共四种继承方式,但是在文章最后我们只需要记住一套代码就行,一定要认真看完。

关于原型链,其实还有很多知识点,我们往往会被一些概念弄混,比如 prototype、constructor 等,但是我在看完《你不知道的 javascript 上》第五章的内容之后,就豁然开朗了,所以本篇文章还是先总结一下关于原型的知识点,然后再总结各种继承方式吧。

二、原型链的基本知识

2.1 对象的内置属性 [[Prototype]]

js 每一个对象都有一个内置属性 [[Prototype]],js 的对象还有其他的内置属性比如 [[class]],之所以是内置属性,意味着我们不能直接通过属性访问点操作符访问,但是我们可以使用其他的方法访问。

比如,对于内置属性 [[Prototype]] 可以使用 Object.getPrototypeOf(obj) 来获取。也可以使用obj.__proto__ 获取,但是已经弃用,已经被  Object.getPrototypeOf(obj) 取代

obj.__proto__ 已弃用

对于内置属性 [[class]] 可以使用 Object.prototype.toString.call(obj) 来获取

注意,这个内置属性是针对对象的,每个对象都有这个内置属性,也可以简单的说【每个对象都有原型】而原型对象又有原型,所以每个对象都有原型链。

注意,js 中所有的变量都是对象,这意味着函数也是对象,所以函数也有一个内置属性[[Prototype]],也可以使用 Object.getPrototypeOf(fn) 来获取,函数的内置属性指向Function.prototype,箭头函数也有内置属性[[Prototype]],因为箭头函数本质也是一个对象。

Object.getPrototypeOf(Array) === Function.prototype // true

2.2 函数的原型 prototype

函数有一个公开可访问不可枚举属性 prototype,指向一个对象,也称之为函数的原型对象。注意三个关键词【公开】【可访问】【不可枚举】

注意,箭头函数没有 prototype 属性!!!这也是箭头函数不能当作构造函数的原因之一!!参考这篇文章

记住,所有的函数(除了箭头函数)都有一个公开可访问的不可枚举的属性 prototype,这意味着可以直接使用 fn.prototype 来获取,这一点和2.1 中说的对象是不同的,对象是不可直接访问的内置属性,函数是可以访问的公开属性。

所以有一个对象和一个函数,你要判断的只能是【函数的 prototype 属性是否在对象的原型链上】

// 有一个函数
function fn() {}
// 有一个对象
let a = new fn()// 判断对象是否在函数的原型链上
Object.getPrototypeOf(a) === fn.prototype // true

2.3 函数的prototype属性的公开可访问不可枚举属性 constructor 

对象有一个公开不可枚举属性 constructor ,翻译过来就是构造函数,注意这个 constructor 是针对对象的,而不是函数的。

函数的 prototype 属性也是一个对象,并且, fn.prototype.constructor = fn

其实,对象本身并没有 .constructor 属性,对象调用 .constructor 的本质是在对象的原型链上找的。再实现继承代码的时候前往别忘了这个 constructor 属性

function fn() {}let a = new fn()fn.prototype.constructor === fn // true 
a.constructor === fn.prototype.constructor // true
a.constructor === fn // true

详细内容还是自己看书吧,书上非常详细!

总结

总之关于原型这块记住三句话

  1. 对象有一个内置属性 [[Prototype]],使用 Object.getPrototypeOf(obj) 获取
  2. 函数有一个公开可访问不可枚举属性 prototype
  3. 函数的 prototype 属性有一个公开可访问的不可枚举属性 constructor,指向函数本身

2.4 原型相关的面试题目

2.4.1 说说你对原型和原型链的理解

回答问题分文两步

(1)原型/原型链是什么?【引用上面的三句话即可】

在 js 中每个对象都有一个内置属性 [[prototype]],可以使用 Object.getPrototypeOf 来获取,指向一个对象;同样的,这个指向的对象也有内置属性[[prototype]] 这样就构成了原型链,原型链最终会指向 Object.prototype,而 Object.prototype 的内置属性 [[prototype]] 指向 null.

同时函数都有一个公开可访问属性 prototype,这个 prototype 属性又有一个 constructor 属性指向函数本身。

(2)原型链有什么用?【属性查找、继承、扩展、属性和方法的共享】

当访问对象的一个属性的时候,如果自身没有找到,就会去原型链上查找,直到找到该属性,或者遍历完完整的原型链,也就是说可以使用原型链实现继承功能。对象可以通过原型链继承父对象的属性或者方法【继承】

也可以使用原型链对对象进行扩展,通过修改原型对象,可以给所有的实例进行属性的增加或修改。如果我们在一个对象的原型上添加属性或者方法,所有基于该原型的实例都会自动继承这些属性和方法,这样可以在不修改每个实例的情况下,实现对对象的扩展【扩展】【注意这一点也是原型链继承的弊端】【也是实例之间属性和方法的共享的方法】

题外话,for ... in 循环就会遍历到对象的原型链上的公开可访问可枚举属性!不能遍历不可枚举属性。

还要注意 for... in 和 for ...of 的区别。

2.4.2 如何获取一个对象的原型对象

(1)从构造函数获取,前提是知道对象的构造函数是谁

(2)使用 Object.getPrototypeOf(obj) 获取

(3)使用 Object.__proto__ 但是官方已经弃用,不建议用了

function fn() {//
}let a = new fn()console.log('a 的原型对象是', fn.prototype)
console.log('a 的原型对象是', Object.getPrototypeOf(a))
console.log('a 的原型对象是', a.__proto__) // 不建议

2.4.3 打印结果

关于原型的面试题,还有各种打印结果的,而且往往和 this 指针、命名提升掺合在一起,所以基础一定要扎实。

随便看一道题目,可能就答不上来

var F = function() {};
Object.prototype.a = function() {console.log('a');
};
Function.prototype.b = function() {console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b()

打印结果是【a、报错 f.b is not a function、 a、 b】 

  1. F 是函数,所有的函数都是 Function 的实例【箭头函数也是】
  2. 函数也是对象,所有对象都是 Object 的实例
  3. f 是对象,所以不会继承 Function,但是作为对象会继承Object

2.4.5 如何使用原型链实现继承,存在什么问题,怎么解决

这个就是本篇文章文章的重点了,不过别担心,最终我们只有一套代码需要记住,前面的都是铺垫!

三、使用原型链继承

3.1 代码实现

原型继承的属性和方法,所以我们在自定义实现的时候,最好是定一个属性,再定义一个方法。而且都会用到 this 指针。

首先要定一个一个子类(函数),一个父类(函数),实现子类继承父类,也就是子类创建的方法可以拥有父类的属性,那么步骤很简单:

  1. 定义一个函数作为父类 Person,并定义一个 name 属性【使用 this 指针】
  2. 给父类原型上加一个方法 getName【使用函数的 prototype 属性 + this 指针】
  3. 定义一个函数作为子类 Student,定一个 gender 属性 【使用 this 指针】
  4. 子类 Student 通过原型继承 Person【使用函数的 prototype 属性 + new 操作符】
  5. 处理子类 Student.prototype 的 constructor,指向 Student
  6. 使用子类创建一个对象 student【使用 new 操作符】
  7. 访问 student.name 和 student.getName
  8. 完成子类 Student 对父类 Person 的属性和方法的继承
function Person() {this.name = 'mike';
}
Person.prototype.getName = function() {return this.name;
}
function Student(gender) {this.gender = gender
}
Student.prototype = new Person();
Student.prototype.constructor = Student;const student = new Student('man');console.log(student.gender);  // 子类自己的属性
console.log(student.name); // 继承父类的属性
console.log(student.getName());  // 继承父类的方法

3.2 存在的问题

面试官肯定会问你这个问题,使用原型继承存在是什么问题?然后再引出怎么解决问题,再引出 es6 中的 class 的继承。

3.2.1 引用类型属性共享问题

原型链继承存在的问题就是,多个子类的实例,指向同一个父类的实例,所以对于父类的引用类型,修改一个子类的实例会影响到其他的实例!【这个问题是可以解决的,具体看第四章】

3.2.2 原型链上所有的属性和方法都是共享的

在原型链中,子类实例共享父类原型对象上的属性和方法。这意味着,如果一个子类实例修改了原型对象上的属性或方法,那么其他所有子类实例也会受到影响,可能会导致意外的副作用。

3.2.3 子类向父类传参需要手动调用父类构造函数

除非我们手动显式的使用 call/apply 方法调用父类的构造函数,否则无法给父类构造函数传递参数。所以传递参数这个问题也是可以解决的,具体看第四章。

3.2.4 无法实现多重继承

一个子类只能继承一个父类,无法实现多重继承

3.2.4 破坏封装性

原型链继承会导致父类的内部属性和方法暴露给子类,从而破坏了封装性。子类可以直接访问父类原型对象上的属性和方法,无法实现严格的控制访问权限。

3.3 总结

使用原型继承,是 es5 中实现继承的必须要学会的,同时还要记住原型继承存在的问题!这个时候就有一个新的问题了,就是如何使用 es5 中的知识解决这些问题。

答案是将四种继承方式组合起来,取各自的优点。不过在此之前我们还是先看看其他的继称方式。

四、构造函数继承

4.1 代码实现

利用 this 指针的显示绑定方法 call 和 apply ,在子类中调用父类构造函数,把父类的成员属性和方法都挂在到子类的 this上。这个方法解决了 3.2.1 和 3.2.3 中的问题。具体代码如下:

function Person(age) {this.name = 'mike';this.age = {num: age}
}Person.prototype.getName = function() {return this.name;
}
function Student(gender, age) {// 手动调用父类的构造函数Person.call(this, age)this.gender = gender
}const student = new Student('man', 12);
const student1 = new Student('women', 25)// 修改第一个实例
student.age.num = 3
console.log('第一个学生', student.age)
console.log('第二个学生', student1.age)

4.2 存在的问题

因为我们没有写下面原型继承的那两句话,所以就无法继承来自父类原型上的属性和方法。

// 构造函数继承没有这两句话
Student.prototype = new Person()
Student.prototype.constructor = Student;

其实我们要继承原型上的属性和方法,写上就行了呗,但是呢,很多教程中都是这样写的,把构造函数继承和原项链继承分开,然后再引出后面的组合继承,那我也就这么弄吧。

五、组合继承

5.1 代码实现

就是把原型链继承和构造函数继承的优点组合起来,完整代码如下。

function Person(age) {this.name = 'mike';this.age = {num: age}
}Person.prototype.getName = function() {return this.name;
}
function Student(gender, age) {// 手动调用父类的构造函数// 构造函数继承Person.call(this, age)this.gender = gender
}// 原型链继承
Student.prototype = new Person()
Student.prototype.constructor = Student;const student = new Student('man', 12);
const student1 = new Student('women', 25)// 修改第一个实例
student.age.num = 3
console.log('第一个学生', student.age, student.getName())
console.log('第二个学生', student1.age, student1.getName())

5.2 存在的问题

每次创建子类实例都执行了两次构造函数 Person.call 和 new Person() ,虽然不影响功能,但是每次创建子类实例,实例的原型中都有两份相同的属性和方法。

这是可以 Object.create 优化的,这就迎来了 es5 继承的最终极版代码。需要有感情的朗读并背诵全文!!

六、寄生式组合继承【必会】

我不喜欢这个名字,因为他听起来很高端的样子,还不如叫 es5 继承终极版!

很简单,把 new Person() 换成 Object.create(Person.prototype)就行了。

function Person(age) {this.name = 'mike';this.age = {num: age}
}Person.prototype.getName = function() {return this.name;
}
function Student(gender, age) {// 重点1Person.call(this, age)this.gender = gender
}// 重点2
Student.prototype = Object.create(Person.prototype)
// 重点3
Student.prototype.constructor = Student;const student = new Student('man', 12);
const student1 = new Student('women', 25)
console.log(Object.getPrototypeOf(student))// 修改第一个实例
student.age.num = 3
console.log('第一个学生', student.age, student.getName())
console.log('第二个学生', student1.age, student1.getName())

这里面其实应用到了,Object.create 的原理,这也是一个面试题目,而且也有可能让你手写一个 Object.create 请看这篇文章。

小结

好吧,整半天就一套代码,如果面试官让你写 es5 的继承,你直接上来就终极版代码安排,我想他应该没有什么可问的了吧,所以你别看概念上那么继承方式那么多,但是实际应用就是一个!一定要记住,可别再翻车了。

那么还有最后一个问题就是 es6 中的继承了!

七、es6 继承

7.1 代码实现

使用类 class + extends 实现继承。主要还是学会使用class 类的各种语法,有几个关键点

  1. class 中只能有一个构造函数 constructor
  2. 可以使用 static 定义静态属性和方法,直接使用类名调用
  3. 子类使用 extends 关键字继承父类,且只能继承一个【说明 es6 原生也不支持多重继承】
  4. 子类在构造函数 constructor 中使用 super 来调用父类的构造函数,并且可以传递参数
  5. 子类中的方法和父类的同名,会覆盖父类的方法
  6. 必须使用 new 操作符,创建 class 示例
class Person {// 定义属性lang = 'zh'// 定义静态属性static nation = 'china'// 构造函数constructor(age) {this.name = 'mike'this.age = {num: age}}// 定义方法getName() {return this.name}// 定义静态方法static getDes () {return 'hello word'}
}class Student extends Person {constructor(gender, age) {super(age)this.gender = gender}
}
const student = new Student('man', 12)
const student1 = new Student('women', 25)
student.age.num = 234console.log('静态属性方法',Person.nation, Person.getDes())
console.log('第一个学生', student.lang, student.getName())
console.log('第二个学生', student1, student.getName())

7.2 面试题目

这个时候肯定会问 es5 中的类和 es6 中的类的区别了,用自己的话总结一些这篇文章的内容即可。

7.2.1 es5 中类 es6 中的继承有什么区别

注意 es6 的class 有一个私有属性和方法,以#开头的,这个倒是不常用。

7.2.2 ts 中的类和 es6 中的类有什么区别

  1. ts 中有类型检查
  2. ts 有访问描述符 private 、public 、protected 等,js 中只有 #开头描述的私有属性
  3. ts 中有抽象类和方法的概念
    1. 抽象类可以包含抽象方法,而接口只能定义方法的签名
  4. ts 支持范型

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/643733.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Golang基础1

基本类型 bool 整数:byte(相当于uint8), rune(相当于int32), int/uint ,int8/uint8 ,int16/uint16 ,int32/uint32 ,int64/uint64 浮点数: float32 ,float64, complex64 ,complex128 array(值类型)、slice、map、chan(引用类型…

Vitis HLS 学习笔记--对于启动时间间隔(II)的理解

目录 1. II的重要性 2. 案例分析 3. 总结 1. II的重要性 在Vitis HLS(High-Level Synthesis)中,启动时间间隔(II,Iteration Interval)是一个非常关键的概念,对于实现高性能的硬件加速器设计…

DS进阶:AVL树和红黑树

一、AVL树 1.1 AVL树的概念 二叉搜索树(BST)虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-…

【linux】chmod权限开放(整个文件夹)

文章目录 起因权限查看权限修改 失败权限修改成功 起因 想要共享conda环境给同事,发现同事没权限。 权限查看 ls #查看当前目录 ls -l # 查看当前目录的东西和权限正常情况下是显示 三个rwx分别属于user,group,others 前面第一个rwx 是针…

刷题之Leetcode242题(超级详细)

242.有效的字母异位词 力扣题目链接(opens new window)https://leetcode.cn/problems/valid-anagram/ 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 示例 1: 输入: s "anagram", t "nagaram" 输出: true 示例 2…

HarmonyOS开发案例:【音乐播放器】

介绍 使用ArkTS语言实现了一个简易的音乐播放器应用,主要包含以下功能: 播放应用中的音频资源文件,并可进行上一曲、下一曲、播放、暂停、切换播放模式(顺序播放、单曲循环、随机播放)等操作。结合后台任务管理模块&…

QML 不同风格和主题的切换

Quick程序提供了方便的用于切换不同风格和主题的配置文件,如果没有设计稿,又想界面没那么丑,那么可以用这套配置,让应用看起来相对专业一点。 一,在 qrc 资源文件中添加 qtquickcontrols2.conf 文件。 二,…

[Algorithm][前缀和][模板 一维前缀和][模板 二维前缀和][寻找数组中心下标][除自身以外数组的乘积] + 前缀和原理 + 前缀和模板

目录 0.原理讲解1.[模板]一维前缀和1.题目链接2.模板代码实现 2.[模板]二维前缀和1.题目链接2.算法原理讲解3.模板代码实现 3.寻找数组的中心下标1.题目链接2.算法原理详解3.代码实现 4.除自身以外数组的乘积1.题目链接2.算法原理详解3.代码实现 0.原理讲解 前缀和:…

(ICML-2021)从自然语言监督中学习可迁移的视觉模型

从自然语言监督中学习可迁移的视觉模型 Title:Learning Transferable Visual Models From Natural Language Supervision paper是OpenAI发表在ICML 21的工作 paper链接 Abstract SOTA计算机视觉系统经过训练可以预测一组固定的预定目标类别。这种受限的监督形式限制…

数新大数据平台迁移解决方案

随着企业的发展和数字化转型的不断深入,企业数据平台建设过去很多年,技术和架构过于落后,原有的大数据平台越来越难以满足业务需求。而在新的技术架构大数据平台的升级过程中,对数据和任务迁移的一致性、完整性有很高的要求&#…

P1106 删数问题

本题为洛谷&#xff1a; #include<iostream> #include<string> using namespace std; int main(){string n;int k;cin>>n>>k;while(k--){for(int i0;i<n.length();i){if(n[i]>n[i1]){n.erase(i,1); break;} }for(int i0;i<n.length()-1&&…

DeepFaceLab小白教程:视频换脸过程

合适那些人阅读&#xff1f; 适合从未使用过DeepFaceLab的群体。 如果你想基于DeepFaceLab完成一次视频换脸的操作&#xff0c;可以看本篇。 下载方式 GitHub https://github.com/iperov/DeepFaceLab 我是用motrix下载。 网盘 https://pan.baidu.com/share/init?surlO4…