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

news2025/1/13 10:31:33

没错这是一道【去哪儿】的面试题目,手写一个 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) {
    // 重点1
    Person.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 = 234

console.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.coloradmin.cn/o/1618326.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器人模型匹配控制(MPC)MATLAB实现

模型匹配控制(Model matching control)是指设计一个控制器使闭环系统的传递函数tf(s)与td(s)相一致! mpcDesigner 可以分为: 2时域精确模型匹配控制3频域精确模型匹配控制 机械臂控制中应用模型匹配控制(Model Matc…

springboot整合mybatis-plus模版

1.创建springboot项目 Maven类型Lombok依赖Spring Web 依赖MySQL Driver依赖pom.xml&#xff1a;<?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/…

开发环境中的调试视图(IDEA)

当程序员写完一个代码时必然要运行这个代码&#xff0c;但是一个没有异常的代码却未必满足我们的要求&#xff0c;因此就要求程序员对已经写好的代码进行调试操作。在之前&#xff0c;如果我们要看某一个程序是否满足我们的需求&#xff0c;一般情况下会对程序运行的结果进行打…

【Nginx】centos和Ubuntu操作系统下载Nginx配置文件并启动Nginx服务详解

目录 &#x1f337; 安装Nginx环境 &#x1f340; centos操作系统 &#x1f340; ubuntu操作系统 &#x1f337; 安装Nginx环境 以下是在linux系统中安装Nginx的步骤&#xff1a; 查看服务器属于哪个操作系统 cat /etc/os-release安装 yum&#xff1a; 如果你确定你的系统…

Linux驱动开发——(四)内核定时器

一、内核的时间管理 1.1 节拍率 Linux内核中有大量的函数需要时间管理&#xff0c;比如周期性的调度程序、延时程序等等&#xff0c;对于驱动编写者来说最常用的是定时器。 硬件定时器提供时钟源&#xff0c;时钟源的频率可以设置&#xff0c;设置好以后就周期性的产生定时中…

Vulnhub靶机 DC-6 打靶实战 详细渗透测试过程

Vulnhub靶机 DC-6 详细渗透流程 打靶实战 目录 Vulnhub靶机 DC-6 详细渗透流程 打靶实战一、将靶机导入到虚拟机当中二、渗透测试主机发现端口扫描信息探测web渗透目录爆破爆破后台密码反弹shell搜集有价值信息SSH远程登录提权反弹jens用户权限的shell 提权利用 一、将靶机导入…

使用kali进行DDos攻击

使用kali进行DDos攻击 1、打开命令提示符&#xff0c;下载DDos-Attack python脚本 git clone https://github.com/Elsa-zlt/DDos-Attack 2、下载好之后&#xff0c;cd到DDos-Attack文件夹下 cd DDos-Attack 3、修改&#xff08;设置&#xff09;对ddos-attack.py文件执行的权…

OpenHarmony实战开发-

简介 SmartPerf-Host是一款深入挖掘数据、细粒度展示数据的性能功耗调优工具&#xff0c;可采集CPU调度、频点、进程线程时间片、堆内存、帧率等数据&#xff0c;采集的数据通过泳道图清晰地呈现给开发者&#xff0c;同时通过GUI以可视化的方式进行分析。该工具当前为开发者提…

理发师问题的业务建模方案

背景 题目&#xff1a; 假设有一个理发店只有一个理发师&#xff0c;一张理发时坐的椅子&#xff0c;若干张普通椅子顾客供等候时坐。没有顾客时&#xff0c;理发师睡觉。顾客一到&#xff0c;叫醒理发师 。如果理发师没有睡觉&#xff0c;而在为别人理发&#xff0c;他就会坐…

【git】多仓库开发

通常我们习惯了在单个仓库下多分支开发&#xff0c;最近用了多仓库开发&#xff0c;我拿一个开源项目举例&#xff0c;总结一下基本流程。 1.fork项目到自己仓库 2.把自己仓库项目pull到本地 3.在本地新建开发分支 git checkout -b dev 4.修改/添加分支对应的远程仓库 修…

数据通信核心

一.认识网络设备 互联网网络设备有AC,AP,防火墙,路由器&#xff0c;交换机等。 这里我们一起了解一下 框式交换机—— 主控板相当于大脑&#xff0c;属于控制平面 交换机网板——数据平面&#xff0c;转发平面——进行不同网卡之间的数据交换&#xff08;设备内部之间的转发…

(2024)Visual Studio的介绍、安装与使用

Visual Studio介绍 1.Visual Studio是什么&#xff1f; Visual Studio是微软公司推出的一款开发工具包系列产品&#xff0c;它是一个基本完整的开发工具集&#xff0c;为软件开发者提供了整个软件生命周期中所需的大部分工具。 2.Visual Studio的定义 Visual Studio是美国微软公…

智慧图书馆为什么用rfid电子标签而不是磁条

智慧图书馆一般都会使用RFID技术&#xff0c;而不是磁条。以下是几个原因&#xff1a; 1. 效率更高&#xff1a;RFID技术可以实现非接触式读取&#xff0c;图书馆工作人员可以同时读取多本书的信息&#xff0c;大大提高了借还书的效率。 2. 数据量更大&#xff1a;RFID标签可以…

多项式和Bezier曲线拟合

目录 1. 多项式拟合2. Bezier曲线拟合3. 源码地址 1. 多项式拟合 在曲线拟合中&#xff0c;多项式拟合方法的性能受到三个主要因素的影响&#xff1a;采样点个数、多项式阶数和正则项。 采样点个数 N N N&#xff1a;从Figure 1中可以看出较少的采样点个数可能导致过拟合&…

npm install 卡在still idealTree buildDeps不动

前言 再使用npm install 安装包依赖时 发现一直卡住 停留在 观察node_cache下的_logs文件 发现一直在拉取包 37 silly idealTree buildDeps 38 silly fetch manifest riophae/vue-treeselect0.4.0尝试解决 尝试设置了taobao镜像源 依然如此 获取已经设置的镜像源 确实是ta…

Vue3:响应式数据的基本使用(ref、reactive)

一、前言 在Vue3中&#xff0c;如果数据不是响应式数据&#xff0c;当数据的值发生改变时&#xff0c;页面上的数据是不会发生改变的。因此本文主要介绍Vue3中响应式数据的使用&#xff0c;包括ref和reactive的基本使用。 二、ref 1、ref —— 创建基本类型的响应式数据 re…

java泛型介绍

Java 泛型是 JDK 5 引入的一个特性&#xff0c;它允许我们在定义类、接口和方法时使用类型参数&#xff0c;从而使代码更加灵活和类型安全。泛型的主要目的是在编译期提供类型参数&#xff0c;让程序员能够在编译期间就捕获类型错误&#xff0c;而不是在运行时才发现。这样做提…

SSH远程连接服务实战

题目&#xff1a; 一.配置两台主机 主机1、 主机名: server.example.com ip: 192.168.78.129 建立用户timinglee&#xff0c;其密码为timinglee 主机2、 主机名&#xff1a;client.example.com ip: 192.168.78.128 2.安需求完成项目 192.168.78.128 在远程登录192.168.78.129的…

为什么我的 Mac 运行缓慢以及如何使用CleanMyMac X修复它

近些年伴随着苹果生态的蓬勃发展&#xff0c;越来越多的用户开始尝试接触Mac电脑。然而很多人上手Mac后会发现&#xff0c;它的使用逻辑与Windows存在很多不同&#xff0c;而且随着使用时间的增加&#xff0c;一些奇奇怪怪的文件也会占据有限的磁盘空间&#xff0c;进而影响使用…

大型集团企业 怎么实现多区域文件交换?

很多大型集团企业&#xff0c;都会在全国各地&#xff0c;甚至海外&#xff0c;都设立分支机构&#xff0c;还有银行、邮政这类机构&#xff0c;都会在全国各地设立多个支行和网点&#xff0c;所以在日常经营过程中&#xff0c;都会存在多区域文件交换的场景。 大型集团企业在进…