1. JavaScript はオブジェクト指向言語です。JavaScript がオブジェクト指向言語であることを説明する前に、オブジェクト指向の 3 つの基本特性、カプセル化、継承、ポリモーフィズムについて説明します。この記事では、JavaScript オブジェクト指向について改めて紹介します。
カプセル化
は、抽象化された属性とメソッドを結合し、属性値は内部で保護され、カプセル化と呼ばれる特定のメソッドを通じてのみ変更および読み取りが可能です
まず、コードを例に挙げてみましょう。では、
name
と
id
という 2 つの属性と、挨拶用の
sayHi
メソッドを持つ
person
コンストラクターを構築します。 :
//定义Person构造函数
function Person(name, id) {
this.name = name;
this.id = id;
}
//在Person.prototype中加入方法
Person.prototype.sayHi = function() {
console.log('你好, 我是' + this.name);
}
Person
构造函数, 它有
name
和
id
两个属性, 并有一个
sayHi
方法用于打招呼:
//实例化对象
let p1 = new Person('阿辉', 1234);
//调用sayHi方法
p1.sayHi();
现在我们生成一个实例对象p1
, 并调用sayHi()
方法
function Person(name, id) {
this.name = name;
this.id = id;
let salary = 20000;
this.getSalary = function (pwd) {
pwd === 123456 ? console.log(salary) : console.log('对不起, 你没有权限查看密码');
}
}
在上述的代码中, p1
这个对象并不知道sayHi()
这个方法是如何实现的, 但是仍然可以使用这个方法. 这其实就是封装. 你也可以实现对象属性的私有和公有, 我们在构造函数中声明一个salary
作为私有属性, 有且只有通过getSalary()
方法查询到薪资.
function Student(name, id, subject) {
//使用call实现父类继承
Person.call(this, name, id);
//添加子类的属性
this.subject = subject;
}
let s1 = new Student('阿辉', 1234, '前端开发');
继承
可以让某个类型的对象获得另一个类型的对象的属性和方法称为继承
以刚才的Person
作为父类构造器, 我们来新建一个子类构造器Student
, 这里我们使用call()
方法实现继承
function Person(name, id) {
this.name = name;
this.id = id;
}
Person.prototype.study = function() {
console.log('我在学习')
};
多态
同一操作作用于不同的对象产生不同的执行结果, 这称为多态
同样以刚才的Person
构造函数为例, 我们为Person
构造函数添加一个study
方法
function Student(name, id, subject) {
//继承Person父类构造函数
Person.call(this, name, id);
//添加子类的属性
this.subject = subject;
//添加study方法
this.study = function() {
console.log('我在学习' + this.subject);
}
}
同样, 我们新建一个Student
构造函数, 该构造函数继承Person
, 并也添加study
方法
let s1 = new Student('阿辉', 1234, '前端开发');
let s2 = new Student('阿傻', 5678, '大数据开发');
s1.study(); //我在学习前端开发
s2.study(); //我在学习大数据开发
此时我们分别实例化s1
和s2
两个学生, 并分别调用study
方法
var student1 = {
name: '阿辉',
age: 22,
subject: '前端开发'
};
var student2 = {
name: '阿傻',
age: 22,
subject: '大数据开发'
};
对于对象s1
和s2
, 我们都调用的是study
ここで、インスタンス オブジェクト p1
を生成し、sayHi()
メソッドを呼び出します
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
console.log(this);
}
上記のコードでは、p1
は、オブジェクトは sayHi()
メソッドがどのように実装されているかを知りませんが、このメソッドは実際には カプセル化 として使用することができます。コンストラクターで salary
をプライベート属性として宣言します。給与は getSalary()
メソッドを通じてのみクエリできます。
Student('阿辉', 22, '前端开发'); //window{}
Inheritance のオブジェクトが許可されます。取得する特定の型 別の型のオブジェクトのプロパティとメソッドは継承と呼ばれます。親クラス コンストラクターとして先ほどの person
を使用して、新しいサブクラス コンストラクター Student) を作成します。ここでは、<code>call()
メソッドを使用して継承を実装します
let student1 = new Student('阿辉', 22, '前端开发'); //Student {name: "阿辉", age: 22, subject: "前端开发"}
ポリモーフィズム
同じ操作が異なるオブジェクトに作用して異なる実行結果を生成することをポリモーフィズムと呼びます
。例として
の Person
コンストラクターを使用します。
study
メソッドを
person
コンストラクターに追加します
let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');
let student3 = new Student('阿呆', 22, 'Python');
let student4 = new Student('阿笨', 22, 'Java');
同様に、新しい Student コンストラクター関数。このコンストラクターは person
を継承し、study
メソッドも追加しますfunction Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
this.study = function() {
console.log('我在学习' + this.subject);
}
}
この時点で、
s1
をインスタンス化します。 > と
s22 人の生徒が、それぞれ <code>study
メソッドを呼び出しました
let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');
student1.study(); //我在学习前端开发
student2.study(); //我在学习大数据开发
オブジェクト s1
と s2
については、私たちは両方とも study
メソッドを呼び出しましたが、結果は一貫していません。これは実際にはポリモーフィズムを実現しています。オブジェクト指向 JavaScript
上記の分析から、JavaScript がオブジェクト指向言語であることが証明できます。なぜなら、それはオブジェクト指向のすべての特性を実装しているからです。実際、オブジェクト指向は単なる概念またはプログラミングのアイデアであり、その存在を特定の言語に依存するべきではありません。言語には、クラス、継承、派生などのメカニズムが実装されていますが、これらのメカニズムはオブジェクト指向を実現するための単なる手段であり、必須ではありません。言い換えれば、言語は、独自の特性に基づいてオブジェクト指向を実装するための適切な方法を選択できます。 ほとんどのプログラマは、最初に Java や C++ などの高級プログラミング言語を学ぶため、「クラス」という実際のオブジェクト指向メソッドを先入観で受け入れ、そのため、クラスベースのオブジェクト指向言語の概念を習慣的に使用して判断します。言語がオブジェクト指向言語であるかどうか。これは、他のプログラミング言語の経験を持つ多くの人が JavaScript オブジェクトを学習するときに難しいと感じる点でもあります。
実際、JavaScript は
プロトタイプ と呼ばれるメソッドを通じてオブジェクト指向プログラミングを実装します。
クラスベースのオブジェクト指向 と
プロトタイプベースのオブジェクト指向 の違いについて説明しましょう。
2. クラスベースのオブジェクト指向とプロトタイプベースのオブジェクト指向の比較
クラスベースのオブジェクト指向
🎜クラス🎜に基づくオブジェクト指向言語ではJava および C++)、🎜クラス🎜と🎜インスタンス🎜に基づいて構築されています。その中で、🎜クラス🎜は、特定の特性を持つオブジェクトのすべてのプロパティを定義します。 🎜クラス🎜は、それが記述するすべてのオブジェクトの中の特定の個体ではなく、抽象的なものです。一方、🎜インスタンス🎜は🎜クラス🎜のインスタンス化であり、そのメンバーです。 🎜🎜🎜プロトタイプ ベースのオブジェクト指向 🎜🎜 この区別は、🎜プロトタイプ ベースの 🎜 言語 (JavaScript など) には存在しません。🎜 オブジェクトがあるだけです。 🎜コンストラクターであっても、インスタンスであっても、プロトタイプ自体もオブジェクトです。プロトタイプベースの言語には、新しいオブジェクトがプリミティブなプロパティを取得できる、いわゆるプロトタイプ オブジェクトの概念があります。 🎜
ということで、JavaScript には非常に興味深い __proto__
属性(ES6以下是非标准属性)用于访问其原型对象, 你会发现,上面提到的构造函数,实例,原型本身都有__proto__
指向原型对象。其最后顺着原型链都会指向Object
这个构造函数,然而Object
的原型对象的原型是null
,不信, 你可以尝试一下Object.prototype.__proto__ === null
为true
。然而typeof null === 'object'
为true
があります。ここまでで、JavaScript のようなプロトタイプベースの言語ではクラスとインスタンスに違いはなく、すべてがオブジェクトである理由が理解できたと思います。
違いの概要
クラスベース (Java) | プロトタイプベース (JavaScript) |
クラスとインスタンスは別のものです。 | すべてのオブジェクトはインスタンスです。 |
クラス定義を通じてクラスを定義し、コンストラクター メソッドを通じてクラスをインスタンス化します。 | コンストラクター関数を通じてオブジェクトのセットを定義および作成します。 |
new 演算子を使用して単一のオブジェクトを作成します。 | 同じ |
クラス定義を通じて既存のクラスのサブクラスを定義し、オブジェクト階層を構築します | オブジェクトをプロトタイプとして指定し、コンストラクターとともにオブジェクト階層を構築します |
クラスのリンクに従ってプロパティを継承します | プロトタイプチェーンに従ってプロパティを継承します |
クラス定義では、クラスのすべてのインスタンスのすべてのプロパティを指定します。実行時にプロパティを動的に追加することはできません | プロパティの初期セットを指定するためのコンストラクター関数またはプロトタイプ。個々のオブジェクトまたはオブジェクト セット全体に対してプロパティを動的に追加または削除できます。 |
二. ES5中的面向对象
*这里的ES5并不特指ECMAScript 5, 而是代表ECMAScript 6 之前的ECMAScript!
(一) ES5中对象的创建
在ES5中创建对象有两种方式, 第一种是使用对象字面量的方式, 第二种是使用构造函数的方式。该两种方法在特定的使用场景分别有其优点和缺点, 下面我们来分别介绍这两种创建对象的方式。
1. 使用对象字面量的方式
我们通过对象字面量的方式创建两个student
对象,分别是student1
和student2
。
var student1 = {
name: '阿辉',
age: 22,
subject: '前端开发'
};
var student2 = {
name: '阿傻',
age: 22,
subject: '大数据开发'
};
上面的代码就是使用对象字面量的方式创建实例对象, 使用对象字面量的方式在创建单一简单对象的时候是非常方便的。但是,它也有其缺点:
在生成多个实例对象时, 我们需要每次重复写name
,age
,subject
属性,写起来特别的麻烦
虽然都是学生的对象, 但是看不出student1
和student2
之间有什么联系。
为了解决以上两个问题, JavaScript提供了构造函数创建对象的方式。
2. 使用构造函数的方式
构造函数就其实就是一个普通的函数,当对构造函数使用new
进行实例化时,会将其内部this
的指向绑定实例对象上,下面我们来创建一个Student
构造函数(构造函数约定使用大写开头,和普通函数做区分)。
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
console.log(this);
}
我特意在构造函数中打印出this
的指向。上面我们提到,构造函数其实就是一个普通的函数, 那么我们使用普通函数的调用方式尝试调用Student
。
Student('阿辉', 22, '前端开发'); //window{}
采用普通方式调用Student
时, this
的指向是window
。下面使用new
来实例化该构造函数, 生成一个实例对象student1
。
let student1 = new Student('阿辉', 22, '前端开发'); //Student {name: "阿辉", age: 22, subject: "前端开发"}
当我们采用new
生成实例化对象student1
时, this
不再指向window
, 而是指向的实例对象本身。这些, 都是new
帮我们做的。上面的就是采用构造函数的方式生成实例对象的方式, 并且当我们生成其他实例对象时,由于都是采用Student
这个构造函数实例化而来的, 我们能够清楚的知道各实例对象之间的联系。
let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');
let student3 = new Student('阿呆', 22, 'Python');
let student4 = new Student('阿笨', 22, 'Java');
(二) ES5中对象的继承
1. prototype
的原型继承
prototype
是JavaScript这类基于原型继承的核心, 只要弄明白了原型和原型链, 就基本上完全理解了JavaScript中对象的继承。下面我将着重的讲解为什么要使用prototype
和使用prototype
实现继承的方式。
为什么要使用prototype
?
我们给之前的Student
构造函数新增一个study
方法
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
this.study = function() {
console.log('我在学习' + this.subject);
}
}
现在我们来实例化Student
构造函数, 生成student1
和`student2
, 并分别调用其study
方法。
let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');
student1.study(); //我在学习前端开发
student2.study(); //我在学习大数据开发
这样生成的实例对象表面上看没有任何问题, 但是其实是有很大的性能问题!我们来看下面一段代码:
console.log(student1.study === student2.study); //false
其实对于每一个实例对象studentx
,其study
方法的函数体是一模一样的,方法的执行结果只根据其实例对象决定(这就是多态),然而生成的每个实例都需要生成一个study
方法去占用一份内存。这样是非常不经济的做法。新手可能会认为, 上面的代码中也就多生成了一个study
方法, 对于内存的占用可以忽略不计。
那么我们在MDN中看一下在JavaScript中我们使用的String
实例对象有多少方法?
上面的方法只是String
实例对象中的一部分方法(我一个屏幕截取不完!), 这也就是为什么我们的字符串能够使用如此多便利的原生方法的原因。设想一下, 如果这些方法不是挂载在String.prototype
上, 而是像上面Student
一样写在String
构造函数上呢?那么我们项目中的每一个字符串,都会去生成这几十种方法去占用内存,这还没考虑Math
,Array
,Number
,Object
等对象!
现在我们应该知道应该将study
方法挂载到Student.prototype
原型对象上才是正确的写法,所有的studentx
实例都能继承该方法。
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
}
Student.prototype.study = function() {
console.log('我在学习' + this.subject);
}
现在我们实例化student1
和student2
let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');
student1.study(); //我在学习前端开发
student2.study(); //我在学习大数据开发
console.log(student1.study === student2.study); //true
从上面的代码我们可以看出, student1
和student2
的study
方法执行结果没有发生变化,但是study
本身指向了一个内存地址。这就是为什么我们要使用prototype
进行挂载方法的原因。接下来我们来讲解一下如何使用prototype
来实现继承。
如何使用prototype
实现继承?
“学生”这个对象可以分为小学生, 中学生和大学生等。我们现在新建一个小学生的构造函数Pupil
。
function Pupil(school) {
this.school = school;
}
那么如何让Pupil
使用prototype
继承Student
呢? 其实我们只要将Pupil
的prototype
指向Student
的一个实例即可。
Pupil.prototype = new Student('小辉', 8, '小学义务教育课程');
Pupil.prototype.constructor = Pupil;
let pupil1 = new Pupil('北大附小');
代码的第一行, 我们将Pupil
的原型对象(Pupil.prototype
)指向了Student
的实例对象。
Pupil.prototype = new Student('小辉', 8, '小学义务教育课程');
代码的第二行也许有的读者会不能理解是什么意思。
Pupil.prototype.constructor = Pupil;
Pupil
作为构造函数有一个protoype
属性指向原型对象Pupil.prototype
,而原型对象Pupil.prototype
也有一个constructor
属性指回它的构造函数Pupil
。如下图所示:
然而, 当我们使用实例化Student
去覆盖Pupil.prototype后
, 如果没有第二行代码的情况下, Pupil.prototype.constructor
指向了Student
构造函数, 如下图所示:
而且, pupil1.constructor
会默认调用Pupil.prototype.constructor
, 这个时候pupil1.constructor
指向了Student
:
Pupil.prototype = new Student('小辉', 8, '小学义务教育课程');
let pupil1 = new Pupil('北大附小');
console.log(pupil1.constructor === Student); //true
这明显是错误的, pupil1
明明是用Pupil
构造函数实例化出来的, 怎么其constructor
指向了Student
构造函数呢。所以, 我们就需要加入第二行, 修正其错误:
Pupil.prototype = new Student('小辉', 8, '小学义务教育课程');
//修正constructor的指向错误
Pupil.prototype.constructor = Pupil;
let pupil1 = new Pupil('北大附小');
console.log(pupil1.constructor === Student); //false
console.log(pupil1.constructor === Pupil); //ture
上面就是我们的如何使用prototype
实现继承的例子, 需要特别注意的: 如果替换了prototype对象, 必须手动将prototype.constructor
重新指向其构造函数。
2. 使用call
和apply
方法实现继承
使用call
和apply
是我个人比较喜欢的继承方式, 因为只需要一行代码就可以实现继承。但是该方法也有其局限性,call
和apply
不能继承原型上的属性和方法, 下面会有详细说明。
使用call
实现继承
同样对于上面的Student
构造函数, 我们使用call
实现Pupil
继承Student
的全部属性和方法:
//父类构造函数
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
}
//子类构造函数
function Pupil(name, age, subject, school) {
//使用call实现继承
Student.call(this, name, age, subject);
this.school = school;
}
//实例化Pupil
let pupil2 = new Pupil('小辉', 8, '小学义务教育课程', '北大附小');
需要注意的是, call
和apply
只能继承本地属性和方法, 而不能继承原型上的属性和方法,如下面的代码所示, 我们给Student
挂载study
方法,Pupil
使用call
继承Student
后, 调用pupil2.study()
会报错:
//父类构造函数
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
}
//原型上挂载study方法
Student.prototype.study = function() {
console.log('我在学习' + this.subject);
}
//子类构造函数
function Pupil(name, age, subject, school) {
//使用call实现继承
Student.call(this, name, age, subject);
this.school = school;
}
let pupil2 = new Pupil('小辉', 8, '小学义务教育课程', '北大附小');
//报错
pupil2.study(); //Uncaught TypeError: pupil2.study is not a function
使用apply
实现继承
使用apply
实现继承的方式和call
类似, 唯一的不同只是参数需要使用数组的方法。下面我们使用apply
来实现上面Pupil
继承Student
的例子。
//父类构造函数
function Student (name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
}
//子类构造函数
function Pupil(name, age, subject, school) {
//使用applay实现继承
Student.apply(this, [name, age, subject]);
this.school = school;
}
//实例化Pupil
let pupil2 = new Pupil('小辉', 8, '小学义务教育课程', '北大附小');
3. 其他继承方式
JavaScript中的继承方式不仅仅只有上面提到的几种方法, 在《JavaScript高级程序设计》中, 还有实例继承,拷贝继承,组合继承,寄生组合继承等众多继承方式。在寄生组合继承中, 就很好的弥补了call
和apply
无法继承原型属性和方法的缺陷,是最完美的继承方法。这里就不详细的展开论述,感兴趣的可以自行阅读《JavaScript高级程序设计》。
三. ES6中的面向对象
基于原型的继承方式,虽然实现了代码复用,但是行文松散且不够流畅,可阅读性差,不利于实现扩展和对源代码进行有效的组织管理。不得不承认,基于类的继承方式在语言实现上更健壮,且在构建可服用代码和组织架构程序方面具有明显的优势。所以,ES6中提供了基于类class
的语法。但class
本质上是ES6提供的一颗语法糖,正如我们前面提到的,JavaScript是一门基于原型的面向对象语言。
(一) ES6中对象的创建
我们使用ES6的class
来创建Student
//定义类
class Student {
//构造方法
constructor(name, age, subject) {
this.name = name;
this.age = age;
this.subject = subject;
}
//类中的方法
study(){
console.log('我在学习' + this.subject);
}
}
//实例化类
let student3 = new Student('阿辉', 24, '前端开发');
student3.study(); //我在学习前端开发
上面的代码定义了一个Student
类, 可以看到里面有一个constructor
方法, 这就是构造方法,而this
关键字则代表实例对象。也就是说,ES5中的构造函数Student
, 对应的是E6中Student
类中的constructor
方法。
Student
类除了构造函数方法,还定义了一个study
方法。需要特别注意的是,在ES6中定义类中的方法的时候,前面不需要加上function
关键字,直接把函数定义进去就可以了。另外,方法之间不要用逗号分隔,加了会报错。而且,类中的方法全部是定义在原型上的,我们可以用下面的代码进行验证。
console.log(student3.__proto__.study === Student.prototype.study); //true
console.log(student3.hasOwnProperty('study')); // false
上面的第一行的代码中, student3.__proto__
是指向的原型对象,其中Student.prototype
也是指向的原型的对象,结果为true
就能很好的说明上面的结论: 类中的方法全部是定义在原型上的。第二行代码是验证student3
实例中是否有study
方法,结果为false
, 表明实例中没有study
方法,这也更好的说明了上面的结论。其实,只要理解了ES5中的构造函数对应的是类中的constructor
方法,就能推断出上面的结论。
(二) ES6中对象的继承
E6中class
可以通过extends
关键字来实现继承, 这比前面提到的ES5中使用原型链来实现继承, 要清晰和方便很多。下面我们使用ES6的语法来实现Pupil
。
//子类
class Pupil extends Student{
constructor(name, age, subject, school) {
//调用父类的constructor
super(name, age, subject);
this.school = school;
}
}
let pupil = new Pupil('小辉', 8, '小学义务教育课程', '北大附小');
pupil.study(); //我在学习小学义务教育课程
上面代码代码中, 我们通过了extends
实现Pupil
子类继承Student
父类。需要特别注意的是,子类必须在constructor
方法中首先调用super
方法,否则实例化时会报错。这是因为子类没有自己的this
对象, 而是继承父类的this
对象,然后对其加工。如果不调用super
方法,子类就得不到this
对象。
四.结束语
JavaScript 被认为是世界上最受误解的编程语言,因为它身披 c 语言家族的外衣,表现的却是 LISP 风格的函数式语言特性;没有类,却实也彻底实现了面向对象。要对这门语言有透彻的理解,就必须扒开其 c 语言的外衣,从新回到函数式编程的角度,同时摒弃原有类的面向对象概念去学习领悟它(摘自参考目录1)。现在的前端中不仅普遍的使用了ES6的新语法,而且在JavaScript的基础上还出现了TypeScript、CoffeeScript这样的超集。可以预见的是,目前在前端生态圈一片繁荣的情况下,对JSer的需求也会越来越多,但同时也对前端开发者的JavaScript的水平提出了更加严苛的要求。使用面向对象的思想去开发前端项目也是未来对JSer的基本要求之一!
相关推荐:
JavaScript面向对象实现猜拳游戏
JavaScript面向对象基础与this指向问题的具体分析
javascript面向对象属性函数用法
以上がオブジェクト指向 JavaScript の新たな理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。