JS面向对象基础(原型链、构造函数、new关键字、寄生组合继承、对象元编程)

news2024/12/22 18:05:46

这篇文章将简单介绍面向对象的基本概念,以及JS语言是如何支持面向对象这种编程范式的,最后还会讲解一些对象元编程的基础知识。通过阅读这篇文章,你可以了解JS中的原型链机制new和构造函数的原理寄生组合继承的实现以及对象元编程的相关知识。

面向对象的概念

面向过程

假设我们想写一个五子棋游戏,如果用面向过程的方式实现,会是类似下面的编码思路。我们创建两个变量分别记录棋盘和当前玩家的信息,然后在一个循环中进行每一步游戏的逻辑。对于一些复杂的逻辑,例如计算游戏是否结束,我们可以单独将其以函数的形式抽象出来。

// 新建一个20*20数组,代表棋盘
const board = new Array(20).fill(0).map(() => new Array(20).fill(''));
let currPlayer = 'black';
while (!isGameEnd(board)) {// 获取玩家所下的位置const [x, y] = getNextStep();// 在棋盘中记录该位置board[x][y] = currPlayer;// 切换当前玩家currPlayer = currPlayer === 'black' ? 'white' : 'black';
}

// 判断游戏是否结束的函数
function isGameEnd(board) {// ...
} 

面向对象

不同于面向过程的编程范式,面向对象将问题抽象为不同对象之间的交互,每个对象都属于某个类,每个类拥有自己的数据和方法。 类相当于现实世界中的一个个概念,而对象则是概念对应的实体,也就是类的实例。

同样以五子棋游戏为例,我们可以声明一个Game类,一个Player类和一个Board类,用来表达五子棋游戏中涉及到的游戏、玩家和棋盘的概念。每个类上有对应的一些属性和方法,例如Game类中有开始游戏和输出游戏结果的方法,以及游戏中的棋盘、玩家和获胜方的数据。

class Board {// 棋盘constructor(size) {this.board = new Array(20).fill(0).map(() => new Array(20).fill(''));}// 计算结果getWinner() {}
}

class Player {constructor(type) {// 玩家所执方this.type = type;// 玩家昵称this.name = name;}// 下子play(board) {}
}

class Game {constructor() {this.board = new Board();this.players = [new Player('black'), new Player('white')];this.currPlayer = this.players[0];this.winner = undefined;}start() {while(!this.winner) {this.currPlayer.play(this.board);this.winner = this.board.getWinner();switchPlayer();}outputWinner();}outputWinner() {}switchPlayer() {}
}

const game = new Game();
game.start(); 

理解面向对象编程,最重要的是要了解在类和对象的基础上发展出的三个特性:封装继承多态。封装也就是把属性和方法都封装在一个对象中;继承让不同的对象可以共享属性和方法;多态则意味着在继承的基础上,不同对象的相同方法可以有不同的表现。

下面的例子中Point3d继承了Point2d的属性xy,也就意味着它自动地拥有了xy属性,因此在其属性声明中我们只要声明z属性就好,这样可以帮助我们减少重复的代码。同时Point3d也继承了calcDistance方法,但我们希望其calcDistance的表现和Point2d的不一样,所以我们重写了它的calcDistance方法,这就是多态的表现。

class Point2d {public x: number;public y: number;calcDistance() {return Math.sqrt(Math.pow(this.x) * Math.pow(this.y))}
}
class Point3d extends Point2d{// 继承了Point2d的x, y属性,因此此处只需声明独有的z属性就好public z: number;// 覆盖了Point2d的calcDistance方法calcDistance() {return Math.sqrt(Math.pow(this.x) + Math.pow(this.y) + Math.pow(this.z))}
} 

接下来我们就来详细地看一下JS中是如何实现面向对象的这三个特性的。

为什么不直接用class关键字?

JS中的class本质上只是语法糖,背后采用的机制是基于原型的继承,通过学习采用原生的方法实现面向对象编程,我们才能更好地理解JS中的原型链、构造函数、new关键字等语言特性,从而写出更好的JS代码。

封装

如果我们想要在JS中实现将一些属性和方法封装起来,很自然地会想到采用JS的Object

假设我们现在要开发一个塔防游戏,采用面向对象的方法,我们创建一个防御塔对象。

const tower1 = {distance: 100,name: 'tower1',fire() {console.log(this.name, ' fired!')}
}

tower1.distance // 100
tower1.fire() // tower1 fired! 

游戏中必然有不止一个防御塔,如果每次创建防御塔都采用这种字面量的形式,代码会变得非常的重复,也不便修改。设想如果我们想给每个tower都加上一个新的生命值属性,那就意味着要到代码中找到每一个字面量去做修改,这是很不方便的。为了解决这个问题,我们可以编写一个专门用于创建tower的函数。

function createTower(distance, name) {return {distance,name,fire() {console.log(this.name, ' fired!')}}
}

const tower1 = createTower(100, 'tower1')
tower1.distance // 100
tower1.fire() // tower1 fired! 

有了这个函数,我们就不用再每一次都通过冗长的字面量来创建Tower对象了。如果想要添加一个新属性,也只要改动一下创建函数就好了。

但这还不够,设想这样一种场景,我们希望敌人攻击tower的时候会触发警报,但如果敌人攻击的是一些其他的设施则不会。这就需要我们能够判断某个对象是否属于Tower类。目前我们创建的这些对象并不能体现他们是属于Tower这个类的。

JS的new操作符可以帮我们解决这个问题。为此我们需要声明一个Tower类的构造函数

// 构造函数必须以大写字母开头
function Tower(distance, name) {this.distance = distance;this.name = name;this.fire = function () {console.log(this.name, ' fires!')}
}

const tower1 = new Tower(100, 'tower1');
const tower2 = createTower(100, 'tower2');
// 判断tower1/tower2是否属于Tower类型
tower1 instanceof Tower; // true
tower2 instanceof Tower; // false 

通过instanceof这个操作符我们可以判断对象是否属于某个类,那么为什么tower1tower2明明有着同样的内容,却只有tower1被判定为属于Tower类呢。我们不妨打印一下他们的具体内容。

可以看到两者的区别主要在于它们的[[prototype]]这个属性不一样,这其实就是我们常说的原型instanceof进行判断的依据就是对象的原型。通过new关键字调用构造函数创建出来的对象,其原型将会指向构造函数的prototype属性对应的对象,默认情况下该对象仅带有constrctor这个属性,指向构造函数本身。

我们可以结合下面这段手写newinstanceof的代码来更好的理解newinstanceof的原理。

function myNew(constructor, ...args) {// 创建一个空对象const obj = {};// 将对象的原型(也就是[[prototype]])设为构造函数的prototypeObject.setPrototypeOf(obj, construtor.prototype);// 将obj作为this调用构造函数const ret = constructor.call(obj, ...args);// 如果调用构造函数后返回了一个对象,就返回那个对象,否则返回objreturn typeof ret === 'object' ? ret : obj;
}

function myInstanceof(left, right) {//基本数据类型直接返回falseif(typeof left !== 'object' || left === null) return false;//getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象let proto = Object.getPrototypeOf(left);while(true) {//查找到原型链尽头,还没找到if(proto == null) return false;//找到相同的原型对象if(proto == right.prototype) return true;proto = Object.getPrototypeOf(proto);}
} 

可以看到,instanceof判断对象类型是通过查看其原型链实现的,其右值是一个构造函数,这是因为理论上某个类的原型对象是无法在代码中直接获取到的,因此需要用构造函数作为指向原型的入口,instanceof在判断时做的事情就是顺着左边对象的原型链进行查看,如果找到了右侧构造函数所对应的原型,就返回true,否则返回false

继承和多态

利用原型管理类方法

设想我们用new Tower创建出了很多个防御塔对象,每个对象上都会绑定一个fire方法,这相当于创建出了大量重复的函数,极大的浪费了内存。我们可以通过将函数挂载到Tower.prototype上来解决这个问题。这里我们利用了JS的原型链机制:当我们访问对象的某个属性时,如果对象本身没有这个属性,JS引擎就会查找该对象的原型,而这个原型正是挂载在对象constructorprototype属性上,在我们的例子中也就是Tower.prototype

// 构造函数必须以大写字母开头
function Tower(distance, name) {this.distance = distance;this.name = name;
}
Tower.prototype.fire = function() {console.log(this.name, ' fired!');
}

const tower1 = new Tower(100, 'tower1');
tower1.fire // tower1 fired! 

通过这种方式,我们创建的每个Tower实例仍然都具有fire方法,但他们是共享同一个fire方法。

原型链

const grandparent = {a: 1,
}

const parent = {b: 2,
}
// 将grandparent设为parent的原型
Object.setPrototypeOf(parent, grandparent)
// 也可以写作 parent.__proto__ = grandparent

const child = {c: 3,
}

// 将parent设为child的原型
Object.setPrototypeOf(child, parent)
// 也可以写作 child.__proto__ = parent

console.log(child.a) // 1
console.log(child.b) // 2
console.log(child.c) // 3 

对象的原型也可以有自己的原型,如果对象的原型上也没有相应的属性,那么JS引擎又会接着查找原型的原型上有没有该属性,以此类推,直到找到该属性或原型为空。这就是我们所说的原型链,利用原型链我们就可以在js中实现继承的机制。

在上面的例子中,我们访问childab属性时,JS引擎就是顺着原型链进行查找从而找到对应的属性的。这三个对象之间的关系如图所示。

我们使用对象字面量创建出来的对象的原型默认为Object.prototype,上面定义了我们在所有对象上都可以调用的方法如toString(), valueOf()等。Object.prototype的原型为null,也就是说所有原型链的查找通常都会到此为止。我们创建的数组的原型都为Array.prototype,数组的相关方法都挂载在该原型上面。Date对象、正则表达式对象等也有自己的原型,而这些原型的原型又都是Object.prototype

方法的继承

function Tower(distance, name) {this.distance = distance;this.name = name;
}
Tower.prototype.fire = function() {console.log(this.name, ' fired!');
}

function IceTower(distance, name, level) {this.distance = distance;this.name = name;this.level = level;
}
IceTower.prototype = Object.create(Tower.prototype, { constructor: IceTower }
);

IceTower.prototype.freeze = function() {console.log(this.name, ' freezed!')
}

const iceTower1 = new IceTower(100, 'ice1');
iceTower1.fire(); // ice1 fired! 

为了继承Tower上的方法,我们需要将IceTower的prototype设定为一个原型为Tower的原型的实例,当我们调用fire方法时,JS引擎就可以顺着原型链找到Tower.prototype,从而触发fire方法,在这里找到了fire方法并进行调用。下面这张图展示了上面代码中各个对象之间的关系。

属性的继承

上面的例子中我们只实现了方法的继承,并没有实现属性的继承。在IceTower的构造函数中我们又写了一遍distancename的挂载逻辑,我们可以通过“借用”父类的构造函数来解决这个问题,也就是在子类的构造函数中调用一次父类的构造函数。通过call方法将this绑定为当前的子类,这样就相当于利用了父类构造函数中的属性绑定代码,实现了代码的复用。

function Tower(distance, name) {this.distance = distance;this.name = name;
}
Tower.prototype.fire = function() {console.log(this.name, ' fired!');
}

function IceTower(distance, name, level) {// 借用父类构造函数,实现属性的继承Tower.call(this, distance, name);this.level = level;
}
// 连接父类的原型,实现方法的继承
IceTower.prototype = Object.create(Tower.prototype, { constructor: IceTower }
)
IceTower.prototype.freeze = function() {console.log(this.name, ' freezed!')
}

const iceTower1 = new IceTower(100, 'ice1');
iceTower1.fire(); // ice1 fired! 

上面这种实现继承的方法被称为寄生组合继承

Class关键字

在了解完如何采用非class语法实现继承之后,我们再来看看ES6的class语法,这也是我更加建议大家在日常编码中使用的语法,前面的内容主要是用于了解一些相关的概念。

class Tower {constructor(distance, name) {this.distance = distance;this.name = name;}fire() {console.log(this.name, ' fires!');}
}

class IceTower extends Tower{constructor(distance, name, level) {super(distance, name);this.level = level;}freeze() {console.log(this.name, ' freezed!');}
}

const iceTower1 = new IceTower(100, 'ice1');
iceTower1.fire(); // ice1 fired! 

通过控制台我们可以看到,类实例的constructor就是类的名称,实例的的方法则挂载在实例的[[prototype]]上。在进行继承时,我们通过super关键字调用了父类的构造函数,和我们在寄生组合式继承时借用父类构造函数的逻辑是类似的。但class语法让我们可以把类声明的相关内容集中在一个代码块中,逻辑更加清晰,也方便来自其它语言的使用者快速上手。

对象元编程

属性枚举

在JS中我们可以利用for/in来枚举对象的属性,这种枚举默认是会将对象原型链上的所有emurable(可枚举)属性也枚举出来,但这通常是不符合预期的。我们可以利用hasOwnProperty这个方法来判断某个属性是属于对象本身还是来自其原型链。

for (let key in obj) {// 过滤来自原型链的属性if (obj.hasOwnProperty(key)) {// do something}
} 

另外一种更方便的方法是利用Object.keys(),这个方法会返回对象所有可枚举的自有属性(ownProperty,也就是不在原型链上的)数组。

属性配置

前面我们讲过所有的对象的原型都是Object.prototype,但当我们枚举一个对象的值时,就算不作过滤也不会枚举出toString, valueOf这些方法,原因在于这些属性的enumerable属性都设置为了false。除了enumerable,每个对象属性都还有configurable, writable两个属性。writable决定了一个属性能否被改变,configurable决定了一个属性能否被删除以及其属性(也就是我们刚刚说的那三个)能否被修改。

通过getOwnPropertyDescriptordefineProperty我们可以实现属性配置的查看和修改。

Object.getOwnPropertyDescriptor(Object.prototype, "toString")
// {
// "writable": true,
// "enumerable": false,
// "configurable": true
// }
const obj = {}
Object.defineProperty(obj, "x", {value: 1, // 对象的值writable: false,enumerable: true,configurable: false,
});

obj.x; // 1
obj.x = 2; // 因为x的writable属性为false,写入后其值并不会改变
obj.x; // 1
delete obj.x; // 返回false,代表删除失败,严格模式下会报错
obj.x; // 1,x仍然存在 

对象冻结

除了对某个属性进行限制之外,有的时候我们会希望能够限制整个对象的一些行为,例如属性的添加、删除、更新等。有三个函数可以帮我们实现这种功能。

  • Object.preventExtensions:限制对象不能再添加新的属性* Object.seal:在前者的基础上,将所有属性的configurable设为false,也就是不能删除* Object.freeze:在前者的基础上,将所有属性的writable设为false,也就是不能修改任何属性除了上述的特性之外,ES6中又引入了Symbol, Reflect, Proxy等元编程特性,感兴趣的同学可以自行了解一下。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

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

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

相关文章

李群李代数学习笔记

前言 因为论文学习的需要,入门了一下李群和李代数,觉得B站的这个视频讲得不错:视频地址为机器人学——李群、李代数快速入门,这里记录一下。 前言引入:一些常见的例子S1S^1S1:单位复数SO(2)SO(2)SO(2)&…

ArcGIS基础实验操作100例--实验64创建统计图符号

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台:ArcGIS 10.6 实验数据:请访问实验1(传送门) 高级编辑篇--实验64 创建统计图符号 目录 一、实验背景 二、实验数据 三、实验步骤 (1&am…

24考研数学复习方法、全年规划

文章目录各个阶段推荐的辅导书和习题1.教材基础:22年9月-23年3月复习“三基”2.强化阶段:23年4月-23年8月3.真题阶段:23年9月-10月4.冲刺模拟阶段:23年11-12月各个阶段推荐的辅导书和习题 阶段(时间)辅导教材习题册1.基础阶段(1-…

Vue初识系列【2】内容升级版

文章目录一 模板语法1.1 文本1.2 原始THTML1.3 属性Attribute1.4 JavaScript表达式的使用二 条件渲染2.1 v−if&v−elsev-if\&v-elsev−if&v−else2.2 v−showv-showv−show2.3 v−ifv-ifv−if与v−showv-showv−show的区别三 列表渲染3.1 v−forv-forv−for列表渲…

OpenSceneGraph几何基础教程【OSG】

默认情况下,OSG 使用顶点数组法和显示列表法来渲染几何体。 但是,渲染策略可能会发生变化,具体取决于几何数据的呈现方式。 在本文中,我们将了解在 OSG 中处理几何体的基本技术。 OpenSceneGraph 后端的 OpenGL 使用几何图元&…

Typora 图床教程(阿里云版)

由于码云现在需要登录才能看到相关图片文件后,导致我们已经不能愉快的使用它作为图床了,所以我们需要使用其他工具来作为图床使用了,本文使用阿里云OSS作为Typora的图床。 阿里云OSS相较于其他几个方法来说最大的优点就是稳定了,…

《图机器学习》-Machine Learning for Graphs

Machine Learning for Graphs一、Application of Graph ML一、Application of Graph ML 图机器学习的任务可以分为四个类型: NodelevelNode\ levelNode level(结点级别)EdgelevelEdge\ levelEdge level(边级别)Community(subgraph)levelCommunity(subgraph)\ level…

【rpm】源码包制作rpm包|修改rpm、重新制作rpm包

目录 前言 安装rpmbuild rpmbuild制作rpm 包 同时生成devel包 修改rpm、重新制作rpm包 RPM 打包 工具 SPEC文件 rpmbuild的目录和Spec宏变量和参数说明 preamble部分 Body 部分 标题宏变量/工作目录 spec文件信息 符号说明 CMake制作rpm包 HelloWorld 更多SPEC…

微信小程序开发——小程序的宿主环境—组件

一.小程序的宿主环境—组件1.小程序中组件的分类小程序中的组件也是由宿主环境提供的,开发者可以基于组件快速搭建出漂亮的页面结构。官方把小程序的组件分为了9大类,分别是:1.视图容器 2.基础内容 3.表单组件 4.导航组件5.媒体组件 6.map 地…

企业寄件管理系统使用教程

专为企业量身打造的寄件管理类平台,也就是企业寄件管理系统。其存在的意义在哪里?又是如何运用的?我们往下看看......讨论它存在的意义在哪里,我们先来看看企业普遍存在的寄件场景痛点:1、最早的手写快递单&#xff0c…

一维差分(例acwing重新排序)

一维差分是为了解决访问一个数组中的几个区间,降低时间复杂度使用的差分就是前缀和的逆运算(a[i]b[1]b[2]…b[i])差分的作用就是快速实现将数组部分加上一个数。例如给定一个数组 A 和一些查询 Li,Ri,求数组中第 Li 至第 Ri 个元素…

Maven高级-属性-版本管理-资源配置-多环境开发配置-跳过测试

Maven高级-属性 4.2)属性类别 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 4.3)属性类别&#xff1a;自定义属性 作用 等同于定义变量&#xff0c;方便统一维护 定义格式&#xff1a; <!--定义自定义属性--> <properties><…

STM32MP157驱动开发——Linux ADC驱动

STM32MP157驱动开发——Linux ADC驱动0.前言一、ADC 简介1.ADC 简介2.STM32MP157 ADC简介二、ADC 驱动源码解析1.设备树下的 ADC 节点2.ADC 驱动源码分析1&#xff09;stm32_adc 结构体2&#xff09;stm32_adc_probe 函数3&#xff09;stm32_adc_iio_info 结构体三、驱动开发1.…

【深度学习】经典算法解读及代码复现AlexNet-VGG-GoogLeNet-ResNet(二)

链接: 【深度学习】经典算法解读及代码复现AlexNet-VGG-GoogLeNet-ResNet(一) 4.GoogLeNet 4.1.网络模型 GoogLeNet的名字不是GoogleNet&#xff0c;而是GoogLeNet&#xff0c;这是为了致敬LeNet。GoogLeNet和AlexNet/VGGNet这类依靠加深网络结构的深度的思想不完全一样。Go…

创建Vue3项目以及引入Element-Plus

创建Vue3项目以及引入Element-Plus 前提条件&#xff1a;本地需要有node环境以及安装了npm&#xff0c;最好设置了镜像&#xff0c;这样下载包的时候会快些。 1、安装vue脚手架vue-cli3 npm install vue/cli -g2、安装后查看vue的版本 vue -V3、创建Vue项目&#xff0c;项目…

通信电子、嵌入式类面试题刷题计划01

文章目录001——什么是奈奎斯特采样定理&#xff1f;002——有源滤波器和无源滤波器的区别是什么&#xff1f;003——什么是反馈电路&#xff1f;请举出相关应用004——什么是竞争冒险现象&#xff1f;如何消除和避免此类现象005——什么是基尔霍夫定理&#xff1f;006——if e…

揣着一口袋的阳光满载而归--爱摸鱼的美工(13)

-----------作者&#xff1a;天涯小Y 揣着一口袋的阳光满载而归&#xff01; 慷懒周末 睡到自然醒&#xff0c;阳光洒在书桌上 套进宽松自在的衣服里 出门&#xff0c;去楼下坐坐 在阳光里吃午餐 在阳光里打个盹 在阳光里看猫咪上蹿下跳 在阳光里点个咖啡外卖 虚度时光&#xf…

【CANN训练营第三季】TBE算子开发

文章目录直播学习结业考核直播学习 安装准备&#xff1a;https://www.hiascend.com/document/detail/zh/mindstudio/50RC3/instg/instg_000022.html 开发参考: https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/600alpha003/operatordevelopment/opdevg/atla…

基础算法(八)——离散化

离散化 介绍 这里的离散化&#xff0c;特指整数的、保序的离散化 有些题目可能需要以数据作为下标来操作&#xff0c;但题目给出的数据的值比较大&#xff0c;但是数据个数比较小。此时就需要将数据映射到和数据个数数量级相同的区间&#xff0c;这就是离散化&#xff0c;即…

Java学习笔记——继承(上)

目录继承入门继承的好处继承的特点继承中成员变量的访问特点this和super访问成员的格式继承中成员方法的访问特点方法重写概述和应用场景方法重写的注意事项权限修饰符继承入门 继承的好处 好处&#xff1a; 提高了代码的复用性。 提高了代码的维护性。 让类与类之间产生了关系…