图片来源于 DigitalOcean
1. 什么是类
在说 JavaScript 的面向对象的实现方法之前,我们先来看面向对象编程的一个核心概念——类(class)。类是对拥有同样属性(property)和行为的一系列对象(object)的抽象。这里说的“行为”,在基于类的面向对象的语言中通常叫做类的方法(method)。而在 JavaScript 里,函数也是“一等公民”,可以被直接赋值给一个变量或一个对象的属性,因此在本文后续的讨论中,把“行为”也归入“属性”的范畴。
2. JavaScript 对“类”的实现
JavaScript 一开始是被设计成在网页上对表单进行校验或者对网页上的元素进行操纵的一种脚本语言,没有像 C++ 和 Java 那样用class
、private
、protected
等关键字来定义类的语法。JavaScript 采用的是一种更简单的实现方式:既然类就是拥有同样属性的一系列对象,那么只要通过一种方式能使某一些对象拥有同样的属性就行了。
JavaScript 规定每一个对象都可以有一个原型([[prototype]]
内部属性)。(在实现 ECMAScript 5.1 规范以前,除了Object.prototype
以外的对象都必须有一个原型。)每个对象都“共享”其原型的属性:在访问一个对象的属性时,如果该对象本身没有这个属性,则 JavaScript 会继续试图访问其原型的属性。这样,就可以通过指定一些对象的原型来使这些对象都拥有同样的属性。从而我们可以这样认为,在 JavaScript 中,以同一个对象为原型的对象就是属于同一个类的对象。
2.1 JavaScript 中对象的原型的指定方式
那么 JavaScript 中的对象与其原型是怎样被关联起来的呢?或者说,JavaScript 中的对象的原型是怎样被指定的呢?
2.1.1 new 操作符
JavaScript 有一个 new 操作符(operator),它基于一个函数来创建对象。这个用 new 操作符创建出来的对象的原型就是 new 操作符后面的函数(称为“构造函数”)的 prototype 属性。例如:
functionB(){} B.prototype=a; varb=newB();vara={"aa":1};
此时 b 对象的原型就是 a 对象。我在另一篇文章中介绍了 new 操作符的具体实现逻辑,供大家参考。
2.1.2 Object.create 方法
Object.create 方法直接以给定的对象作为原型创建对象。一个代码例子:
varb=Object.create(a);vara={"aa":1};
此时 b 对象的原型就是 a 对象。关于 Object.create 方法的实现细节,大家可参考我的这篇文章。
2.1.3 Object.setPrototypeOf 方法
new 操作符和 Object.create 方法都是在创建一个对象的同时就指定其原型。而 Object.setPrototypeOf 方法则是指定一个已被创建的对象的原型。代码例子:
varb=Object.create(a); //此时b的原型是a varc={"cc":2}; Object.setPrototypeOf(b,c); //此时b的原型变为c了vara={"aa":1};
2.1.4 隐式指定
数字、布尔值、字符串、数组和函数在 JavaScript 中也是对象,而它们的原型是被 JavaScript 隐式指定的:
1. 数字(例如1
、1.1
、NaN
、Infinity
)的原型是Number.prototype
;
2. 布尔值(true
和false
)的原型为Boolean.prototype
;
3. 字符串(例如""
、"abc"
)的原型为String.prototype
;
4. 函数(例如function () {}
、function (a) { return a + '1'; }
) 的原型为Function.prototype
;
5. 数组(如[]
、[1, '2']
)的原型是Array.prototype
;
6. 用花括号直接定义的对象(如{}
,{"a": 1}
)的原型是Object.prototype
。
2.2 JavaScript 中定义类的代码示例
下面给出定义一个类的一段 JavaScript 代码的示例。它定义一个名为 Person 的类,它的构造函数接受一个字符串的名称,还一个方法 introduceSelf 会输出自己的名字。
functionPerson(name){ this.name=name; } Person.prototype.introduceSelf=function(){ console.log("Mynameis"+this.name); }; //----====类定义结束====---- //下面实例化一个Person类的对象 varsomeone=newPerson("Tom"); //此时someone的原型为Person.prototype someone.introduceSelf();//输出MynameisTom//----====类定义开始====----
如果转换为 ECMAScript 6 引入的类声明(class declaration)语法,则上述 Person 类的定义等同于:
constructor(name){ this.name=name; } introduceSelf(){ console.log("Mynameis"+this.name); } }classPerson{
2.3 对“构造函数”的再思考
在上面的例子中,假如我们不通过Person.prototype
来定义 introduceSelf 方法,而是在构造函数中给对象指定一个 introduceSelf 属性:
this.name=name; this.introduceSelf=function(){ console.log("Mynameis"+this.name); }; } varsomeone=newPerson("Tom"); someone.introduceSelf();//也会输出MynameisTomfunctionPerson(name){
虽然这种方法中,通过 Person 构造函数 new 出来的对象也都有 introduceSelf 属性,但这里 introduceSelf 变成了 someone 自身的一个属性而不是 Person 类的共有的属性:
this.name=name; } Person1.prototype.introduceSelf=function(){ console.log("Mynameis"+this.name); }; vara=newPerson1("Tom"); varb=newPerson1("Jerry"); console.log(a.introduceSelf===b.introduceSelf);//输出true deletea.introduceSelf; a.introduceSelf();//仍然会输出MynameisTom,因为introduceSelf不是a自身的属性,不会被delete删除 b.introduceSelf=function(){ console.log("Iamapig"); }; Person1.prototype.introduceSelf.call(b);//输出MynameisJerry //即使b的introduceSelf属性被覆盖,我们仍然可以通过`Person1.prototype`来让b执行Person1类规定的行为。functionPerson1(name){
this.name=name; this.introduceSelf=function(){ console.log("Mynameis"+this.name); }; } a=newPerson2("Tom"); b=newPerson2("Jerry"); console.log(a.introduceSelf===b.introduceSelf);//输出false //a的introduceSelf属性与b的introduceSelf属性是不同的对象,分别占用不同的内存空间。 //因此这种方法会造成内存空间的浪费。 deletea.introduceSelf; a.introduceSelf();//会抛TypeError b.introduceSelf=function(){ console.log("Iamapig"); }; //此时b的行为已经与Person2类规定的脱节,对象a和对象b看起来已经不像是同一个类的对象了functionPerson2(name){
但是这种方法也不是一无是处。例如我们需要利用闭包来实现对 name 属性的封装时:
this.introduceSelf=function(){ console.log("Mynameis"+name); }; } varsomeone=newPerson("Tom"); someone.name="Jerry"; someone.introduceSelf();//输出MynameisTom //introduceSelf实际用到的name属性已经被封装起来,在Person构造函数以外的地方无法访问 //name相当于Person类的一个私有(private)成员属性functionPerson(name){
3. JavaScript 的类继承
类的继承实际上只需要实现:
1. 子类的对象拥有父类定义的所有成员属性;
2. 子类的任何一个构造函数都必须在开头调用父类的构造函数。
实现第 2 点的方式比较直观。而怎样实现第 1 点呢?其实我们只需要让子类的构造函数的 prototype 属性(子类的实例对象的原型)的原型是父类的构造函数的 prototype 属性(父类的实例对象的原型),简而言之就是:把父类实例的原型作为子类实例的原型的原型。这样在访问子类的实例对象的属性时,JavaScript 会沿着原型链找到子类规定的成员属性,再找到父类规定的成员属性。而且子类可在子类构造函数的 prototype 属性中重载(override)父类的成员属性。
3.1 代码示例
下面给出一个代码示例,定义一个 ChinesePerson 类继承上文中定义的 Person 类:
Person.apply(this,name);//调用父类的构造函数 } ChinesePerson.prototype.greet=function(other){ console.log(other+"你好"); }; Object.setPrototypeOf(ChinesePerson.prototype,Person.prototype);//将Person.prototype设为ChinesePerson.prototype的原型 varsomeone=newChinesePerson("张三"); someone.introduceSelf();//输出“Mynameis张三” someone.greet("李四");//输出“李四你好”functionChinesePerson(name){
上述定义 ChinesePerson 类的代码改用 ECMAScript 6 的类声明语法的话,就变成:
constructor(name){ super(name); } greet(other){ console.log(other+"你好"); } }classChinesePersonextendsPerson{
3.1.1 重载父类成员属性的代码示例
你会不会觉得上面代码示例中,introduceSelf 输出半英文半中文挺别扭的?那我们让 ChinesePerson 类重载 introduceSelf 方法就好了:
console.log("我叫"+this.name); }; varsomeone=newChinesePerson("张三"); someone.introduceSelf();//输出“我叫张三” varother=newPerson("BaWang"); other.introduceSelf();//输出MynameisBaWang //ChinesePerson的重载并不会影响父类的实例对象ChinesePerson.prototype.introduceSelf=function(){
文中的兔纸图片由作者提供,
推荐阅读:
前端项目框架搭建随笔 --- Webpack 踩坑记
freeCodeCamp 高级算法
[译文] 如何在 JavaScript 中更好地使用数组
如果觉得《javascript 本地对象和内置对象_JavaScript 的面向对象》对你有帮助,请点赞、收藏,并留下你的观点哦!