首页 前端开发 Web前端入门第 67 问:JavaScript 中的面向对象编程

Web前端入门第 67 问:JavaScript 中的面向对象编程

此 对象 非彼对象啊,不要理解错了哦~~

面向对象编程 这个概念在 Java 编程语言中用得比较多,JS 同时支持 面向对象编程 和 函数式编程。

像大名鼎鼎的 React 和 Vue 他们都有两种开发风格,比如:

Vue 中的 组合式API 和 选项式API 也是两种编程模式的代表。

React 中的 函数式组件 和 类组件 就是两种编程模式的代表。

原型链
JS 中的每个对象(null 除外)都有一个隐式原型,可以通过 __proto__ 或者 Object.getPrototypeOf() 访问。

null 虽然用 typeof 检测会获得 Object 类型,但 null 有一点特殊,表示空,什么都没有的意思。

比如:

const a = 'str'
console.log(a.__proto__) // 获得 String.prototype
console.log(Object.getPrototypeOf(a)) // 获得 String.prototype
console.log(a.__proto__ === String.prototype) // true
console.log(Object.getPrototypeOf(a) === String.prototype) // true
这么多年的搬砖经验来看,__proto__ 这个属性能派上用场的场景真的少见~~

构造函数
function 申明的函数都拥有一个显式原型 prototype 属性,如果用 new 关键字调用这个函数,那么此时这个函数就称之为 构造函数。

实例化构造函数的时候,实例化对象的 __proto__ 就指向构造函数的 prototype 属性。

function Person() {}
Person.prototype.name = '前端路引'

const person = new Person()

console.log(person.__proto__ === Person.prototype) // true
console.log(person.name) // 输出:前端路引
编程实践推荐:构造函数声明时,首字母一般大写,而函数声明时首字母一般小写。

继承
继承 这玩意儿可以算作面向对象编程的核心思想,如果编程语言不支持 继承,那面向对象就是一句空话。

JS 中的继承玩法多种多样,掌握一种就可以独步武林~~ 但面试官可是全能高手,一般都会问知道有几种继承方式,他们怎么实现这些问题。

原型链继承
子类通过 prototype 指向父类实例,就是原型链继承,但此种方式继承有一个大缺陷,会共享父类中的引用类型(比如数组、对象)。

function Parent() {
// 父类中申明的属性
this.arr = ['公众号', '前端路引'];
}

// 父类中申明的方法
Parent.prototype.getName = function () {
console.log('前端路引');
}

function Child() {}
// 使用原型链继承父类
Child.prototype = new Parent();

const child1 = new Child();
// 修改 child1 的实例属性
child1.arr.push(1);
child1.getName();

const child2 = new Child();
child2.getName();
console.log(child2.arr); // 输出:['公众号', '前端路引', 1]
可以看到子类都可以调用父类的 getName 方法,但是在 child1 实例修改 arr 属性后,child2 也会受影响,这边是原型链继承中的弊端。

构造函数继承
此继承方式的特点是利用函数的 call 或者 apply 方法,再传入子类的 this 指针实现继承,缺点是无法继承父类上的原型方法。

function Parent(name) {
this.name = name;
this.arr = ['公众号', '前端路引'];
this.test = function () {
console.log('调用父类 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}

function Child(name) {
Parent.call(this, name);
}

const child1 = new Child('前端路引');
// 修改 child1 的实例属性
child1.arr.push(1);
console.log(child1.arr); // ['公众号', '前端路引', 1]
child1.test(); // 输出:调用父类 test 方法

const child2 = new Child('前端路引');
console.log(child2.arr); // ['公众号', '前端路引']
child2.getName(); // 报错 TypeError: child2.getName is not a function
此继承方式修复了 原型链继承 中共享 引用类型 问题,但却存在无法继承父类原型链方法的弊端。

组合继承
此继承方式结合了原型链继承和构造函数继承而衍生出的另一种继承方式,同时解决了两种继承方式的弊端。

function Parent(name) {
this.name = name;
this.arr = ['公众号', '前端路引'];
this.test = function () {
console.log('调用父类 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}

function Child(name) {
Parent.call(this, name); // 继承属性(第二次调用)
}
Child.prototype = new Parent(); // 继承方法(第一次调用)

const child1 = new Child('前端路引');
// 修改 child1 的实例属性
child1.arr.push(1);
console.log(child1.arr); // ['公众号', '前端路引', 1]
child1.test(); // 输出:调用父类 test 方法

const child2 = new Child('前端路引');
console.log(child2.arr); // ['公众号', '前端路引']
child2.getName(); // 输出:前端路引
组合继承可以拥有父类上的 getName,同时还不会共享父类上的引用类型,但父类构造函数却被调用了两次,存在性能优化上的空间,这也是此种继承方式的弊端。

寄生组合继承
此继承方式通过 Object.create 方法复制父类的原型链,优化父类会被调用两次问题,算是比较完美的一种继承方式,不存在性能浪费。

function Parent(name) {
this.name = name;
this.arr = ['公众号', '前端路引'];
this.test = function () {
console.log('调用父类 test 方法');
}
}
Parent.prototype.getName = function () {
console.log(this.name);
}

function Child(name) {
Parent.call(this, name); // 继承属性(第二次调用)
}
Child.prototype = Object.create(Parent.prototype); // 继承原型
Child.prototype.constructor = Child; // 修复子类的 constructor 引用

const child1 = new Child('前端路引');
// 修改 child1 的实例属性
child1.arr.push(1);
console.log(child1.arr); // ['公众号', '前端路引', 1]
child1.test(); // 输出:调用父类 test 方法

const child2 = new Child('前端路引');
console.log(child2.arr); // ['公众号', '前端路引']
child2.getName(); // 输出:前端路引
寄生组合继承重点就是两句代码:

Child.prototype = Object.create(Parent.prototype); // 继承原型
Child.prototype.constructor = Child; // 修复子类的 constructor 引用
由于 Object.create 会复制父类的 constructor 属性,导致子类的 constructor 属性被重写了,所以需要手动修复。

在 ES6 出现之前,这种继承已经是 JS 面向对象编程中的最优解了。

ES6 class 继承
ES6 出现了 class 类关键字,也多了 extends 继承关键字,可以很方便的实现继承。

但其底层实现逻辑还是 寄生组合继承,相当于是提供了一种语法糖,简化了寄生组合继承中的代码。

class Parent {
constructor(name) {
this.name = name;
this.arr = ['公众号', '前端路引'];
}
test () {
console.log('调用父类 test 方法');
}
getName () {
console.log(this.name);
}
}

class Child extends Parent {
constructor(name) {
super(name); // 必须有此行
}
}

const child1 = new Child('前端路引');
// 修改 child1 的实例属性
child1.arr.push(1);
console.log(child1.arr); // ['公众号', '前端路引', 1]
child1.test(); // 输出:调用父类 test 方法

const child2 = new Child('前端路引');
console.log(child2.arr); // ['公众号', '前端路引']
child2.getName(); // 输出:前端路引
如果没有 super() 这行代码,JS 解析器会报错:

ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
意思就是在父类访问 this 之前,子类中必须调用 super() 方法。

原型链查找规则
有了父子两层继承关系,那肯定就有更多层次的继承关系,比如:

class A {}

class B extends A {}

class C extends B {}
在这种多层级的继承关系中,JS 的原型链查找规则永远都是一层一层往上找,终点是找到 Object.prototype 为止,如果还找不到就报错。

比如:

class A {}
class B extends A {}
class C extends B {}

const child = new C();
console.log(child.toString())
console.log(child.test()) // 报错 TypeError: child.test is not a function
以上代码中 A、B、C 都没有 toString 方法,但是实例 child 却可以调用,原因就是 child 的原型链最终找到了 Object.prototype.toString 方法。

而 test 直到 Object.prototype 为止都没找到,所以最终报错。

可以理解其查找规则是这样的:

实例 (obj) --> 构造函数.prototype --> 父构造函数.prototype --> ... --> Object.prototype --> null
写在最后
虽然个人更喜欢 函数式编程 方式,但面向对象这种写法也必须要掌握,要不然看到面向对象的代码,就玩完了~~

站星网

此 对象 非彼对象啊,不要理解错了哦~~面向对象编程 这个概念在 Java 编程语言中用得比较多,JS 同时支持 ..

为您推荐

Web前端入门第 57 问:JavaScript 数据类型与类型转换

在程序语言中,数据类型是基础,一切程序都是建立在基础数据之上。如果说程序如同万丈高楼平地起,那么数据类型就像沙、石、钢筋、水泥等等最基础的原料。一样的高楼,不同的人,用相同的原料,造的方法也会有千般变..

JavaScript实现图片上传预览及获取图片尺寸和大小的完整指南

在现代Web开发中,提供图片上传的预览功能以及获取图片的尺寸和大小信息,已成为提升用户体验的重要手段。本文将详细介绍如何使用JavaScript实现这些功能,帮助你在用户上传图片前进行有效的验证和优化。一、实现图..

Web前端入门第 60 问:JavaScript 各种数组定义与数组取值方法

数组可以算是程序里面最常用的数据结构了,但凡网页上任何一个列表数据,基本都是以数组的形式存在,像表格、banner图、菜单列表、商品列表,分类列表等等,在前端领域都是以数组处理。数组的定义JS 的数组花样很多..

Web前端入门第 55 问:JavaScript 严格模式与非严格模式区别

JavaScript 默认是非严格模式的,可以通过 "use strict"; 启用严格模式。此声明语句可以放在 JS 文件顶部,也可以放在函数内部。启用严格模式1、外部脚本在 JS 文件开头声明,内部脚本在 <script> 标签开头声明,声..

Web前端入门第 53 问:JavaScript 的各种调试方法

任何一门编程语言,在学习之前都应该先弄清楚它的调试方法,毕竟没有不挖坑的人类!程序一旦出现问题,第一时间就是找到问题出在哪儿,其次才是拿出解决办法。如果都找不到问题原因,那又何从谈起解决办法呢?如何排..

Web前端入门第 54 问:JavaScript 3 种书写位置及 script 标签的正确存放位置

JS 的代码并没有强制规定放在 HTML 中的某个位置,如果您有使用过开发者工具查看过网页源码,那么您会看到很多 JS 代码都以 .js 文件的形式存放,并且放在了 HTML 文件最后,也就是 </body> 结束标签之前。但如果仔..

Web前端入门第 52 问:JavaScript 的应用领域

截至目前,您应该对前端的 HTML + CSS 应该有了很清楚的认知,至少实现一个静态网页已经完全不在话下了。当然,CSS 功能绝不止这些,一些不太常用的 CSS 相关知识,后续将通过案例进行分享。那么咱们接下来看看 Java..

.NET C# 过滤从富文本编辑器html里的Javascript脚本

富文本编辑器在允许用户输入丰富内容的同时,也带来了跨站脚本攻击(XSS)的风险。过滤提交的 HTML 中的 <script> 脚本是防止跨站脚本攻击(XSS)的关键步骤。在 .NET C# 服务端过滤 <script> 脚本主要有以下几种方..

轻松学习 JavaScript函数中的默认参数

JavaScript函数可以有默认参数值。通过默认函数参数,你可以初始化带有默认值的正式参数。如果不初始化具有某些值的参数,则该参数的默认值为undefined。请看下列代码:function foo(num1){console.log(num1);}foo()..

JavaScript API 设计原则详解

前言本篇博文来自一次公司内部的前端分享,从多个方面讨论了在设计接口时遵循的原则,总共包含了七个大块。系卤煮自己总结的一些经验和教训。本篇博文同时也参考了其他一些文章,相关地址会在后面贴出来。很难做到详..

JavaScript 中精度问题以及解决方案

JavaScript 中的数字按照 IEEE 754 的标准,使用 64 位双精度浮点型来表示。其中符号位 S,指数位 E,尾数位M分别占了 1,11,52 位,并且在ES5 规范中指出了指数位E的取值范围是[-1074, 971]。精度问题汇总想用有限..

JavaScript 六种继承方式

继承是面向对象编程中又一非常重要的概念,JavaScript支持实现继承,不支持接口继承,实现继承主要依靠原型链来实现的。原型链首先得要明白什么是原型链,在一篇文章看懂proto和prototype的关系及区别中讲得非常详细..

JavaScript 事件委托详解

基本概念事件委托,通俗地来讲,就是把一个元素响应事件(click、focus……)的函数委托到另一个元素;一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应..

JavaScript 中的遍历详解

编程这么多年,要是每次写遍历代码时都用 for 循环,真心感觉对不起 JavaScript 语言~对象遍历为了便于对象遍历的测试,我在下面定义了一个测试对象obj。测试对象// 为 Object 设置三个自定义属性(可枚举)Object.p..

web前端开发2018年12月找工作总结

2018年的冬天额外的冷,由内致外...作为一名刚刚踏入社会的实习生,可谓是狠狠的体验了一把什么叫社会(同时也感叹父母赚钱真的很不容易)前几天看见这样一句话"如果你不知道社会的辛苦,要么是有人替你扛了,要么是还没轮..

值得探索的 8 个机器学习 JavaScript 框架

JavaScript开发人员倾向于寻找可用于机器学习模型训练的JavaScript框架。下面是一些机器学习算法,基于这些算法可以使用本文中列出的不同JavaScript框架来模型训练:简单的线性回归多变量线性回归逻辑回归朴素贝叶斯..

JavaScript 保留两位小数

以下我们将为大家介绍 JavaScript 保留两位小数的实现方法:四舍五入以下处理结果会四舍五入:var num =2.446242342;num = num.toFixed(2); // 输出结果为 2.45不四舍五入以下处理结果不会四舍五入:第一种,先把小数边..

JavaScript 页面跳转、页面重定向

JavaScript 实现页面跳转重定向可以使用以下两种方法:window.location.replace("url")类似 HTTP 重定向将地址替换成新 url,该方法通过指定 URL 替换当前缓存在历史里(客户端)的项目,因此当使用 replace 方法之..

在 Javascript 中 声明时用 var 与不用 var 的区别

avascript 声明变量的时候,虽然用 var 关键字声明和不用关键字声明,很多时候运行并没有问题,但是这两种方式还是有区别的。可以正常运行的代码并不代表是合适的代码。var num = 1;是在当前域中声明变量。如果在方..

javascript中call()、apply()、bind()的用法终于理解

其实是一个很简单的东西,认真看十分钟就从一脸懵B 到完全 理解!先看明白下面:例 1obj.objAge; // 17obj.myFun() // 小张年龄 undefined例 2shows() // 盲僧 比较一下这两者 this 的差别,第一个打印里面的 this ..

发表回复

返回顶部

微信分享

微信分享二维码

扫描二维码分享到微信或朋友圈

链接已复制