客路青山 發表於 2025-6-20 10:32:00

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

<p>此 <code>对象</code> 非彼<strong>对象</strong>啊,不要理解错了哦~~</p>
<p><code>面向对象编程</code> 这个概念在 Java 编程语言中用得比较多,JS 同时支持 <code>面向对象编程</code> 和 <code>函数式编程</code>。</p>
<p>像大名鼎鼎的 <code>React</code> 和 <code>Vue</code> 他们都有两种开发风格,比如:</p>
<p>Vue 中的 <code>组合式API</code> 和 <code>选项式API</code> 也是两种编程模式的代表。</p>
<p>React 中的 <code>函数式组件</code> 和 <code>类组件</code> 就是两种编程模式的代表。</p>
<h2 id="原型链">原型链</h2>
<p>JS 中的每个对象(null 除外)都有一个隐式原型,可以通过 <code>__proto__</code> 或者 <code>Object.getPrototypeOf()</code> 访问。</p>
<p><code>null</code> 虽然用 <code>typeof</code> 检测会获得 Object 类型,但 <code>null</code> 有一点特殊,表示空,什么都没有的意思。</p>
<p>比如:</p>
<pre><code class="language-js">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
</code></pre>
<p>这么多年的搬砖经验来看,<code>__proto__</code> 这个属性能派上用场的场景真的少见~~</p>
<h3 id="构造函数">构造函数</h3>
<p>function 申明的函数都拥有一个显式原型 <code>prototype</code> 属性,如果用 <code>new</code> 关键字调用这个函数,那么此时这个函数就称之为 <code>构造函数</code>。</p>
<p>实例化构造函数的时候,实例化对象的 <code>__proto__</code> 就指向构造函数的 <code>prototype</code> 属性。</p>
<pre><code class="language-js">function Person() {}
Person.prototype.name = '前端路引'

const person = new Person()

console.log(person.__proto__ === Person.prototype) // true
console.log(person.name) // 输出:前端路引
</code></pre>
<p>编程实践推荐:构造函数声明时,首字母一般<strong>大写</strong>,而函数声明时首字母一般<strong>小写</strong>。</p>
<h2 id="继承">继承</h2>
<p><code>继承</code> 这玩意儿可以算作面向对象编程的核心思想,如果编程语言不支持 <code>继承</code>,那面向对象就是一句空话。</p>
<p>JS 中的继承玩法多种多样,掌握一种就可以独步武林~~ 但面试官可是全能高手,一般都会问知道有几种继承方式,他们怎么实现这些问题。</p>
<h3 id="原型链继承">原型链继承</h3>
<p>子类通过 <code>prototype</code> 指向父类实例,就是原型链继承,但此种方式继承有一个大缺陷,会共享父类中的引用类型(比如数组、对象)。</p>
<pre><code class="language-js">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]
</code></pre>
<p>可以看到子类都可以调用父类的 <code>getName</code> 方法,但是在 <code>child1</code> 实例修改 <code>arr</code> 属性后,<code>child2</code> 也会受影响,这边是<strong>原型链继承</strong>中的弊端。</p>
<h3 id="构造函数继承">构造函数继承</h3>
<p>此继承方式的特点是利用函数的 <code>call</code> 或者 <code>apply</code> 方法,再传入子类的 this 指针实现继承,缺点是无法继承父类上的原型方法。</p>
<pre><code class="language-js">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
</code></pre>
<p>此继承方式修复了 <code>原型链继承</code> 中共享 <code>引用类型</code> 问题,但却存在无法继承父类原型链方法的弊端。</p>
<h3 id="组合继承">组合继承</h3>
<p>此继承方式结合了原型链继承和构造函数继承而衍生出的另一种继承方式,同时解决了两种继承方式的弊端。</p>
<pre><code class="language-js">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(); // 输出:前端路引
</code></pre>
<p>组合继承可以拥有父类上的 <code>getName</code>,同时还不会共享父类上的引用类型,但父类构造函数却被调用了两次,存在性能优化上的空间,这也是此种继承方式的弊端。</p>
<h3 id="寄生组合继承">寄生组合继承</h3>
<p>此继承方式通过 <code>Object.create</code> 方法复制父类的原型链,优化父类会被调用两次问题,算是比较完美的一种继承方式,不存在性能浪费。</p>
<pre><code class="language-js">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(); // 输出:前端路引
</code></pre>
<p>寄生组合继承重点就是两句代码:</p>
<pre><code class="language-js">Child.prototype = Object.create(Parent.prototype); // 继承原型
Child.prototype.constructor = Child; // 修复子类的 constructor 引用
</code></pre>
<p>由于 <code>Object.create</code> 会复制父类的 <code>constructor</code> 属性,导致子类的 <code>constructor</code> 属性被重写了,所以需要手动修复。</p>
<p>在 ES6 出现之前,这种继承已经是 JS 面向对象编程中的<strong>最优解</strong>了。</p>
<h3 id="es6-class-继承">ES6 class 继承</h3>
<p>ES6 出现了 <code>class</code> 类关键字,也多了 <code>extends</code> 继承关键字,可以很方便的实现继承。</p>
<p>但其底层实现逻辑还是 <code>寄生组合继承</code>,相当于是提供了一种语法糖,简化了寄生组合继承中的代码。</p>
<pre><code class="language-js">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(); // 输出:前端路引
</code></pre>
<p>如果没有 <code>super()</code> 这行代码,JS 解析器会报错:</p>
<pre><code class="language-bash">ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
</code></pre>
<p>意思就是在父类访问 this 之前,子类中必须调用 <code>super()</code> 方法。</p>
<h2 id="原型链查找规则">原型链查找规则</h2>
<p>有了父子两层继承关系,那肯定就有更多层次的继承关系,比如:</p>
<pre><code class="language-js">class A {}

class B extends A {}

class C extends B {}
</code></pre>
<p>在这种多层级的继承关系中,JS 的原型链查找规则永远都是一层一层往上找,终点是找到 <code>Object.prototype</code> 为止,如果还找不到就报错。</p>
<p>比如:</p>
<pre><code class="language-js">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
</code></pre>
<p>以上代码中 A、B、C 都没有 <code>toString</code> 方法,但是实例 child 却可以调用,原因就是 child 的原型链最终找到了 <code>Object.prototype.toString</code> 方法。</p>
<p>而 <code>test</code> 直到 Object.prototype 为止都没找到,所以最终报错。</p>
<p>可以理解其查找规则是这样的:</p>
<pre><code class="language-bash">实例 (obj) --&gt; 构造函数.prototype --&gt; 父构造函数.prototype --&gt; ... --&gt; Object.prototype --&gt; null
</code></pre>
<h2 id="写在最后">写在最后</h2>
<p>虽然个人更喜欢 <code>函数式编程</code> 方式,但面向对象这种写法也必须要掌握,要不然看到面向对象的代码,就玩完了~~</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>&nbsp;</p>
<p style="font-size: 18px;font-weight: bold;">文章首发于微信公众号【<span style="color:rgb(255, 71, 87)">前端路引</span>】,欢迎 <span style="color:#4ec259">微信扫一扫</span> 查看更多文章。</p>
<p>
<img style="max-width: 320px;" src="https://images.cnblogs.com/cnblogs_com/linx/2447020/o_250228035031_%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81.png"/>
</p>
<p>本文来自博客园,作者:前端路引,转载请注明原文链接:https://www.cnblogs.com/linx/p/18937897</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/linx/p/18937897
頁: [1]
查看完整版本: Web前端入门第 67 问:JavaScript 中的面向对象编程