但行好事 莫问前程
前言🎀
JavaScript是一门支持面向对象的语言,我们可以自由的在开发中使用类、对象与继承,这使得代码具有更好的扩展性和复用性。
可能你已经习惯于在代码中使用 class / new / extends
,它们仿佛与其他语言的类没什么区别:
scala
// Java
public class Father { ... }
public class Son extends Father {
public Son() {
super();
}
...
}
Son son = new Son();
// JavaScript
class Father { constructor(...) {} }
class Son extends Father {
constructor(...) {
super();
}
...
}
const son = new Son();
但在ES6之前实现这些操作并不是一件简单的事情,它涉及到 对象、函数 之间的应用 和 JavaScript中特有的原型机制,而且它们的实现可能与你预想的很不一样。
本文我们一起学习 面向对象与原型 的相关知识,提升对JavaScript的理解,希望能对你有所帮助~
面向对象 OOP
面向对象 是当前主流的编程范式,让我们(开发者)能在软件中对真实世界的事物进行抽象建模,其核心思想可以概括为:封装、继承、多态。
根据面向对象的思想,我们将客观的事物根据特征抽象为 对象 ,并将有相同特征的一系列对象规定为 类 ,之后以类为模板 实例化 出具体对象,且类通过 继承 机制支持扩展和重写。
而JavaScript作为一门支持面向对象的编程语言,自然需要实现上述的 类、实例化、继承。
Class
类是创建对象的模板,包含属性和方法的定义,开发中,我们通常先定义类再实例化对象。
JavaScript的设计中并没有 类(class) 关键字,仅能通过 构造函数 来模拟类,基于 原型 实现继承
虽然ECMAScript6中引入了class
、extends
关键字,让开发者可以像其他语言一样使用类,但这其实只是一种语法糖,其底层实现仍然是构造函数和原型。
JavaScript
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
}
}
// 等价于
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
};
构造函数
首字母大写、用于创建对象 的函数通常被称为构造函数,但构造函数与普通函数的区别只有是否使用new
关键字调用。
使用new
关键字的函数调用会变成 "构造函数调用",创建对象实例并返回:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
}
}
const p1 = new Person('xiaoming', 20);
console.log(p1); // Person {name: 'xiaoming', age: 20, sayHello: ƒ}
const p2 = new Person('xiaohong', 18);
console.log(p2); // Person {name: 'xiaohong', age: 18, sayHello: ƒ}
console.log(p1 === p2); // false
通过 new
和 构造函数,JavaScript 初步模拟了类,实现了类的实例化。
但如果没有 继承机制,JavaScript 中的类只是一个空架子
继承
继承机制 允许我们从现有类中创建一个新类并 继承它的属性和方法 ,且支持对继承到的内容进行重写和扩展。
继承建立了类与类之间的关系,形成逻辑和现实世界对象之间的关系模型。我们可以通过子类继承父类,进一步对真实事物进行描述。
JavaScript
class Person { ... }
// Programmer类 继承于 Person类
class Programmer extends Person {
constructor(name, age, language) {
super(name, age);
// 扩展
this.language = language;
}
// 重写
sayHello() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old. I'm a ${this.language} programmer.`);
}
}
const person1 = new Person('John', 30);
const programmer1 = new Programmer('Jane', 25, 'JavaScript');
person1.sayHello(); // "Hello, my name is John, I'm 30 years old."
programmer1.sayHello(); // "Hello, my name is Jane, I'm 25 years old. I'm a JavaScript programmer."
而在JavaScript中,继承是基于 原型 来实现的。
原型
传统面向对象语言中继承意味着复制操作,但 JavaScript(默认)并不会复制对象属性,相比之下JavaScript的继承更像是委托而不是复制。
JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。
而对象之间的关联是通过原型来实现的,接下来我们一起学习JavaScript是如何实现这套继承机制的~
原型对象
在JavaScript中,每个函数在被定义的同时会生成一个原型对象 ,并赋值给函数的 prototype
属性。
prototype
prototype
是函数特有的属性,指向了原型对象。
原型对象包含了类的公共属性和方法 ,且原型对象自身会生成 constructor
属性与创建它的函数关联。
根据原型对象,我们重写前文的构造函数:
JavaScript
function Person(name, age) {
this.name = name;
this.age = age;
- this.sayHello = function () {
- console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
- }
}
+ Person.prototype.sayHello = function() {
+ console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
+ };
之后,在实例对象中可以正常使用sayHello
方法,即使该方法不在实例自身:
JavaScript
const person1 = new Person('xiaoming', 20);
person1.hasOwnProperty('sayHello'); // false
person1.sayHello(); // "Hello, my name is xiaoming, I'm 20 years old."
Q:为什么实例对象可以访问到自身不存在的属性和方法?
A:访问属性时,引擎会调用内部的默认[[Get]]
操作, [[Get]]
操作会检查对象本身是否包含这个属性,如果没找到会去对象的 proto 属性上查找。
此外这种方式可以节省内存,并且代码更易于维护
proto
JavaScript 实例化对象时,会为每个对象添加一个内部属性__proto__
并指向构造函数的原型对象prototype
,使对象可以访问原型对象中的属性和方法。
person1.__proto__ === Person.prototype; // true
注:每个对象都有
__proto__
属性,但只有函数对象才有prototype
属性
原型链
原型对象也是对象,自身也有__proto__
属性指向原型对象的原型,就这样,原型对象之间形成了一个链条,我们把这种链式结构称为原型链。
所以[[Get]]
操作更准确的讲是会从对象自身开始,去遍历查找对象的原型链。
原型链的尽头
进一步展开对象的原型链,我们可以看到正常情况下(原型对象是可以被覆盖的)原型对象prototype
的__proto__
属性指向了Object
的原型对象。
而Object
函数的原型对象的原型(即Object.prototype.__proto__
)指向了null,所以我们可以认为原型链的尽头是null。
JavaScript
person1.__proto__.__proto__.__proto__ === null
此外,函数也是对象,函数对象同样拥有__proto__
属性,指向Function
函数的原型对象,即 Person.__proto__ === Function.prototyoe
,而Function.prototyoe
的__proto__
指向了Object.prototype
,尽头最终仍是指向了null。
总结
对于JavaScript的继承来说,"委托" 是一个很合适的术语,因为对象之间的关系不是复制而是委托,而委托则是借助原型机制实现的。
面向对象:
- JavaScript 是一门支持面向对象的语言,需要实现 类 与 继承
- 类 可以为模板 实例化 出具体对象,继承 使类支持 扩展 和 重写
- JavaScript 中
class、extends
只是语法糖,本质上是 构造函数 和 继承 - 任何使用
new
关键字 的函数调用会变成 "构造函数调用",会创建对象实例并返回
原型与原型链:
- 每个函数在定义时会生成 原型对象 并赋值给函数的
prototype
属性 - 原型对象:包含了类的公共属性和方法,生成的同时创建
constructor
属性指向函数 - 每个对象存在代表原型的内部属性
__proto__
,指向它构造函数的原型对象prototype
- 原型对象同样存在
__proto__
属性,指向原型对象的原型 prototype
是函数属性,__proto__
是对象属性__proto__
将原型对象连接起来组成了原型链[[Get]]
操作时会从对象自身开始,遍历查找对象的原型链Object
是原型链的顶端,它的原型对象是null
- 函数对象的 原型
__proto__
指向 Function原型对象Function.prototype
题外话
部分内容没有深入讨论,更深的细节还需自己钻研,例如:
- 实现class的 寄生组合式继承
new
调用函数的流程、this
的指向问题hasOwnProperty
判断属性和方法、Object.create
创建纯净的空对象- . . . . . .
更多JavaScript相关知识,欢迎查阅:
# 【JavaScript】详解作用域与闭包🚀案例+图解
# 【JavaScript】面试高频代码原理与实现,防抖节流、深拷贝、继承 、Promise....
结语🎉
不要光看不实践哦,希望本文能对你有所帮助~
如果有收获还望 关注+点赞+收藏 🌹
才疏学浅,如有问题或建议还望指教!