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

news/2024/9/20 1:19:32/ 标签: javascript, 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]],因为箭头函数本质也是一个对象。

javascript">Object.getPrototypeOf(Array) === Function.prototype // true

2.2 函数的原型 prototype

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

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

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

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

javascript">// 有一个函数
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 属性

javascript">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__ 但是官方已经弃用,不建议用了

javascript">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 指针、命名提升掺合在一起,所以基础一定要扎实。

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

javascript">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 的属性和方法的继承
javascript">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 中的问题。具体代码如下:

javascript">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 存在的问题

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

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

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

五、组合继承

5.1 代码实现

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

javascript">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)就行了。

javascript">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 示例
javascript">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.ppmy.cn/news/1433529.html

相关文章

无人机+光电吊舱:4K AI 180倍混合变焦吊舱技术详解

无人机搭载吊舱是一种常见的配置方式,吊舱可以装载不同的设备,以满足不同的任务需求。吊舱通常挂载在无人机的下方或侧面,可以根据需要进行调整。 随着无人机技术的飞速进步,4K AI 180倍混合变焦吊舱技术的出现,将无人…

C++进阶--异常

C语言传统的处理方式 终止程序:在发生错误时直接终止程序的运行,可以通过assert宏来进行实现。如assert(condition),其中condition不满足要求时,将会使程序立刻停止执行,并输出相关错误信息。这种方式的确定是用户很难…

计算机网络4——网络层1

文章目录 一、网络层1、概念2、网络层的两个层面1)介绍2)问题3)解决 二、网际协议IP1、介绍2、虚拟互联网络1)介绍2)案例 3、IP地址1)IP 地址及其表示方法2)分类的IP地址3)无分类编址…

视频滚动字幕一键批量轻松添加,解锁高效字幕编辑,提升视频质量与观众体验

视频已成为我们获取信息、娱乐休闲的重要渠道。一部成功的视频作品,除了画面精美、音质清晰外,字幕的添加也是至关重要的一环。字幕不仅能增强视频的观感,还能提升信息的传达效率,让观众在享受视觉盛宴的同时,更加深入…

探究C++20协程(5)——基于挂起实现无阻塞的定时器

实现目标 当用传统的线程 sleep 函数来让程序等待时,实际上是在阻塞当前线程。阻塞意味着这个线程在指定的时间(例如100毫秒)内无法执行任何其他任务。这种方式虽然简单,但效率低下,因为它导致CPU资源在等待期间未被充…

使用uni-app开发app时遇到mqtt.js不可用的问题

使用uni-app开发app时遇到mqtt.js不可用的问题 1 问题背景 基于 Vue3 版本创建了 uni-app 项目用于开发微信小程序,项目中用到了 mqtt.js(v4.1.0),编译为微信小程序能够正常运行,但是编译为 APP 后,控制台…

C# winform OpenProtocol中数据中的UI是什么类型?

C# winform OpenProtocol中数据中的UI是什么类型?

Nginx安装withSSL模块

Nginx安装withSSL模块 Nginx 配置文件,开启ssl访问时,报出错误信息: nginx: [emerg] the “ssl” parameter requires ngx_http_ssl_module in /usr/local/nginx/conf/nginx_proxy.mimvp.com.conf:76 原因分析: nginx缺少http_ssl_module…

Unity系统学习笔记

文章目录 1.基础组件的认识1.0.组件继承关系图1.1.项目工程文件结构,各个文件夹都是做什么的?1.2.物体变化组件1.2.3.三维向量表示方向1.2.4.移动物体位置附录:使用变换组件实现物体WASD移动 1.3.游戏物体和组件的显示和禁用1.3.1.界面上的操…

数据结构 - 顺序表实现通讯录

test.c文件 #define _CRT_SECURE_NO_WARNINGS 1#include "Contact.h" int main() {Con myContacts;ConInit(&myContacts);int choice;int index;char targetName[100];PerInfo contact; // 创建一个新的联系人信息实例while (1) {printf("\n--- 通讯录管理…

PaddleSeg (2) 模型训练

已处理好数据集和配置文件,可以开始模型训练。 启动训练 python tools/train.py --config configs/xxx.yml --do_eval --use_vdl --save_interval 500 --save_dir output/xxx上述训练命令解释:* `--config`

java spring 07 createBean()和doCreateBean()

01.createBean方法 protected Object createBean(String beanName, RootBeanDefinition mbd, Nullable Object[] args)throws BeanCreationException {if (logger.isTraceEnabled()) {logger.trace("Creating instance of bean " beanName "");}RootBea…

引导过程和服务控制

1、Linux系统开机引导过程 1)开机自检 检测硬件设备,找到能够引导系统的设备,比如硬盘 2)MBR引导 运行MBR扇区里的主引导程序GRUB 3)启动GRUB菜单 系统读取GRUB配置文件(/boot/grub2/grub.cfg)获取内…

spring boot 定义启动页 到 login

当前办法只是针对 项目启动后 直接跳转到 指定静态页面 如果有验证身份 安全等问题 可以另外想办法 去添加 ,需要的直接 拉过去使用 修改 【"redirect: 需要启动后访问到文件位置得地址 ”】 直接上代码 : import org.springframework.context…

【教程】使用vitepress搭配githubPages构建自己的在线笔记

1. 创建VitePress项目 确保自己已经安装好了node,我这个笔记用的是node 18.16.0, 怎么安装nvm这个可以csdn或者掘金,再或者等我有空了我就更新一下 使用nvm安装node # 查看可用版本 nvm list avaliable # 安装node nvm install 18.16.0 # 切换node nvm …

(C语言)sscanf 与 sprintf详解

目录 1.sprintf函数详解 2. sscanf函数详解 1.sprintf函数详解 头文件&#xff1a;stdio.h 作用&#xff1a;将格式化的数据写入字符串里&#xff0c;也就是将格式化的数据转变为字符串。 演示&#xff1a; #include <stdio.h> struct S {char name[10];int height;…

无人机探测技术,无人机侦测频谱仪技术实现详解

频谱仪&#xff0c;又称为频谱分析仪&#xff0c;是一种用于测量电信号频谱特性的仪器。其基本原理是通过将时域信号转换为频域信号&#xff0c;进而分析信号的频率成分、功率分布、谐波失真等参数。频谱仪利用快速傅里叶变换&#xff08;FFT&#xff09;算法&#xff0c;将采集…

Github 2024-04-24 C开源项目日报 Top9

根据Github Trendings的统计,今日(2024-04-24统计)共有9个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量C项目9C++项目1我的电视 - 安卓电视直播软件 创建周期:40 天开发语言:CStar数量:649 个Fork数量:124 次关注人数:649 人贡献人数:1 人Open…

STM32 USB HID报告描述符没有报告长度

STM32 USB HID设置(STM32CubeMX)_我也想成大侠的博客-CSDN博客 不影响鼠标功能

面向对象设计模式

设计模式通常被分为三种类型&#xff1a;创建型模式、结构型模式和行为型模式。 创建型模式 创建型模式主要关注对象的创建机制&#xff0c;它们提供了一种将对象创建和实例化的机制&#xff0c;使得系统在不直接依赖于具体类的情况下能够灵活地创建对象。 创建型模式的典型…