>  기사  >  웹 프론트엔드  >  객체지향 JavaScript에 대한 새로운 이해

객체지향 JavaScript에 대한 새로운 이해

小云云
小云云원래의
2018-02-08 16:31:081543검색

1. JavaScript는 객체 지향 언어입니다. JavaScript가 객체 지향 언어임을 설명하기 전에 객체 지향의 세 가지 기본 특성인 Encapsulation, Inheritance, Polymorphism 을 살펴보겠습니다. 이번 글에서는 객체지향 자바스크립트를 다시 소개하겠습니다.

Encapsulation

은 추상화된 속성과 메서드를 결합하며, 속성 값은 캡슐화라고 하는 특정 메서드를 통해서만 변경하고 읽을 수 있습니다.

먼저 코드를 예로 들어 보겠습니다. , nameid라는 두 가지 속성이 있는 Person 생성자와 인사말을 위한 sayHi 메서드를 구성합니다. :Person构造函数, 它有nameid两个属性, 并有一个sayHi方法用于打招呼:

//定义Person构造函数
function Person(name, id) {
  this.name = name;
  this.id = id;
}

//在Person.prototype中加入方法
Person.prototype.sayHi = function() {
  console.log('你好, 我是' +  this.name);
}

现在我们生成一个实例对象p1, 并调用sayHi()方法

//实例化对象
let p1 = new Person('阿辉', 1234);

//调用sayHi方法
p1.sayHi();

在上述的代码中, p1这个对象并不知道sayHi()这个方法是如何实现的, 但是仍然可以使用这个方法. 这其实就是封装. 你也可以实现对象属性的私有和公有, 我们在构造函数中声明一个salary作为私有属性, 有且只有通过getSalary()方法查询到薪资.

function Person(name, id) {
  this.name = name;
  this.id = id;
  let salary = 20000;
  this.getSalary = function (pwd) {
    pwd === 123456 ? console.log(salary) : console.log('对不起, 你没有权限查看密码');
  }
}

继承

可以让某个类型的对象获得另一个类型的对象的属性和方法称为继承

以刚才的Person作为父类构造器, 我们来新建一个子类构造器Student, 这里我们使用call()方法实现继承

function Student(name, id, subject) {
  //使用call实现父类继承
  Person.call(this, name, id);
  //添加子类的属性
  this.subject = subject;
}

let s1 = new Student('阿辉', 1234, '前端开发');

多态

同一操作作用于不同的对象产生不同的执行结果, 这称为多态

同样以刚才的Person构造函数为例, 我们为Person构造函数添加一个study方法

function Person(name, id) {
  this.name = name;
  this.id = id;
}

Person.prototype.study = function() {
  console.log('我在学习')
};

同样, 我们新建一个Student构造函数, 该构造函数继承Person, 并也添加study方法

function Student(name, id, subject) { 
  //继承Person父类构造函数
  Person.call(this, name, id);
  //添加子类的属性
  this.subject = subject;
  //添加study方法
  this.study = function() {
    console.log('我在学习' + this.subject);
  }
}

此时我们分别实例化s1s2两个学生, 并分别调用study方法

let s1 = new Student('阿辉', 1234, '前端开发');
let s2 = new Student('阿傻', 5678, '大数据开发');
s1.study(); //我在学习前端开发
s2.study(); //我在学习大数据开发

对于对象s1s2, 我们都调用的是study

var student1 = {
  name: '阿辉',
  age: 22,
  subject: '前端开发'
};

var student2 = {
  name: '阿傻',
  age: 22,
  subject: '大数据开发'
};
이제 인스턴스 객체 p1를 생성하고 sayHi() 메서드를 호출합니다

function Student (name, age, subject) {
  this.name = name;
  this.age = age; 
  this.subject = subject;
  console.log(this);
}
위 코드에서 p1은 객체는 sayHi() 메소드가 어떻게 구현되는지 알지 못하지만 이 메소드를 계속 사용할 수 있습니다. 이것은 실제로 encapsulation
입니다. 또한 우리가 하는 것처럼 객체의 개인 및 공용 속성을 구현할 수도 있습니다. 생성자에서 salary를 개인 속성으로 선언하고 급여는 getSalary() 메소드를 통해서만 쿼리할 수 있습니다.
Student('阿辉', 22, '前端开发'); //window{}

Inheritance는 다음의 객체를 허용합니다. 획득할 특정 유형 다른 객체 유형의 속성과 메소드를 상속이라고 합니다. 이제 Person을 상위 클래스 생성자로 사용하여 새로운 하위 클래스 생성자 Student를 생성합니다. code>. 여기서는 <code>call() 메서드를 사용하여 상속을 구현합니다

let student1 = new Student('阿辉', 22, '前端开发'); //Student {name: "阿辉", age: 22, subject: "前端开发"}

다형성

동일한 작업이 다른 객체에 적용되어 다른 실행 결과를 생성합니다

마찬가지로, 지금은 Person 생성자를 예로 사용하여 Person 생성자에 study 메서드를 추가합니다

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를 인스턴스화합니다. > 및 s2두 명의 학생이 각각 <code>study 메서드를 호출했습니다

let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');

student1.study(); //我在学习前端开发
student2.study(); //我在学习大数据开发
s1s2 개체의 경우, 둘 다 연구 방법을 호출했지만 결과가 일치하지 않아 실제로 다형성을 달성했습니다.객체 지향 JavaScript위의 분석을 통해 JavaScript가 객체 지향 언어임을 입증할 수 있습니다. 실제로 객체지향은 개념이나 프로그래밍 아이디어일 뿐이므로 특정 언어에 의존해서는 안 됩니다. 언어는 클래스, 상속, 파생 등을 구현합니다. 그러나 이러한 메커니즘은 객체 지향을 달성하기 위한 수단일 뿐 필수는 아닙니다. 즉, 언어는 자신의 특성에 따라 객체지향을 구현하는 적절한 방법을 선택할 수 있습니다. 대부분의 프로그래머들은 자바, C++ 등 고급 프로그래밍 언어를 먼저 배우기 때문에 '클래스'라는 실제 객체지향 방식을 선입견적으로 받아들이기 때문에 습관적으로 클래스 기반 객체지향 언어의 개념을 사용해 판단한다. 언어가 객체지향 언어인지 여부. 다른 프로그래밍 언어 경험이 있는 많은 사람들이 자바스크립트 객체를 학습할 때 어려움을 느끼는 부분이기도 합니다. 실제로 JavaScript는 prototype이라는 메서드를 통해 객체 지향 프로그래밍을 구현합니다. 클래스 기반 객체 지향 프로토타입 기반 객체 지향

의 차이점에 대해 논의해 보겠습니다.

2 클래스 기반 객체 지향과 프로토타입 기반 객체 지향의 비교
클래스 기반 객체 지향

🎜객체 지향 언어에서는 🎜클래스🎜를 기반으로 합니다. Java 및 C++) On 🎜class🎜 및 🎜instance🎜를 기반으로 구축되었습니다. 그중 🎜class🎜는 특정 특성을 가진 객체의 모든 속성을 정의합니다. 🎜클래스🎜는 그것이 설명하는 모든 객체 중 특정 개인이 아닌 추상적인 것입니다. 반면에 🎜 인스턴스 🎜는 🎜 클래스 🎜의 인스턴스화이며 그 멤버입니다. 🎜🎜🎜프로토타입 기반 객체 지향 🎜🎜 JavaScript와 같은 🎜프로토타입 기반 🎜 언어에는 이러한 구별이 존재하지 않습니다. 🎜객체만 있습니다! 🎜생성자든, 인스턴스든, 프로토타입 자체든 객체입니다. 프로토타입 기반 언어에는 새로운 객체가 기본 속성을 얻을 수 있는 소위 프로토타입 객체라는 개념이 있습니다. 🎜

자바스크립트에는 매우 흥미로운 __proto__属性(ES6以下是非标准属性)用于访问其原型对象, 你会发现,上面提到的构造函数,实例,原型本身都有__proto__指向原型对象。其最后顺着原型链都会指向Object这个构造函数,然而Object的原型对象的原型是null,不信, 你可以尝试一下Object.prototype.__proto__ === nulltrue。然而typeof null === 'object'true이 있습니다. 이쯤 되면 JavaScript와 같은 프로토타입 기반 언어에서는 클래스와 인스턴스 사이에 차이가 없는데 모든 것이 객체인 이유를 이해하실 수 있으실 거라 믿습니다!

차이점 요약

클래스 기반(Java) 프로토타입 기반(JavaScript) 클래스와 인스턴스는 서로 다릅니다. 모든 객체는 인스턴스입니다. 클래스 정의를 통해 클래스를 정의하고 생성자 메서드를 통해 클래스를 인스턴스화합니다. 생성자 함수를 통해 객체 세트를 정의하고 생성하세요. new 연산자를 통해 단일 개체를 만듭니다. 동일클래스 정의를 통해 기존 클래스의 하위 클래스를 정의하여 객체 계층 구조 구축 객체를 프로토타입으로 지정하고 생성자와 함께 객체 계층 구조 구축 속성을 상속하려면 클래스 링크를 따라가기 프로토타입 체인을 따라 속성을 상속하세요 클래스 정의는 클래스의 모든 인스턴스의 모든 속성을 지정합니다. 속성은 런타임초기 속성 집합을 지정하기 위한 생성자 함수 또는 프로토타입에 동적으로 추가될 수 없습니다. 개별 객체 또는 전체 객체 세트에 속성을 동적으로 추가하거나 제거할 수 있습니다.

二. ES5中的面向对象

*这里的ES5并不特指ECMAScript 5, 而是代表ECMAScript 6 之前的ECMAScript!

(一) ES5中对象的创建

在ES5中创建对象有两种方式, 第一种是使用对象字面量的方式, 第二种是使用构造函数的方式。该两种方法在特定的使用场景分别有其优点和缺点, 下面我们来分别介绍这两种创建对象的方式。

1. 使用对象字面量的方式

我们通过对象字面量的方式创建两个student对象,分别是student1student2

var student1 = {
  name: '阿辉',
  age: 22,
  subject: '前端开发'
};

var student2 = {
  name: '阿傻',
  age: 22,
  subject: '大数据开发'
};

上面的代码就是使用对象字面量的方式创建实例对象, 使用对象字面量的方式在创建单一简单对象的时候是非常方便的。但是,它也有其缺点:

  • 在生成多个实例对象时, 我们需要每次重复写name,age,subject属性,写起来特别的麻烦

  • 虽然都是学生的对象, 但是看不出student1student2之间有什么联系。

为了解决以上两个问题, 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实例对象有多少方法?

객체지향 JavaScript에 대한 새로운 이해

上面的方法只是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);
}

现在我们实例化student1student2

let student1 = new Student('阿辉', 22, '前端开发');
let student2 = new Student('阿傻', 22, '大数据开发');

student1.study(); //我在学习前端开发
student2.study(); //我在学习大数据开发

console.log(student1.study === student2.study); //true

从上面的代码我们可以看出, student1student2study方法执行结果没有发生变化,但是study本身指向了一个内存地址。这就是为什么我们要使用prototype进行挂载方法的原因。接下来我们来讲解一下如何使用prototype来实现继承。

如何使用prototype实现继承?

“学生”这个对象可以分为小学生, 中学生和大学生等。我们现在新建一个小学生的构造函数Pupil

function Pupil(school) {
  this.school = school;
}

那么如何让Pupil使用prototype继承Student呢? 其实我们只要将Pupilprototype指向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。如下图所示:

객체지향 JavaScript에 대한 새로운 이해

然而, 当我们使用实例化Student去覆盖Pupil.prototype后, 如果没有第二行代码的情况下, Pupil.prototype.constructor指向了Student构造函数, 如下图所示:
객체지향 JavaScript에 대한 새로운 이해

而且, 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. 使用callapply方法实现继承

使用callapply是我个人比较喜欢的继承方式, 因为只需要一行代码就可以实现继承。但是该方法也有其局限性,callapply不能继承原型上的属性和方法, 下面会有详细说明。

使用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, '小学义务教育课程', '北大附小');

需要注意的是, callapply只能继承本地属性和方法, 而不能继承原型上的属性和方法,如下面的代码所示, 我们给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高级程序设计》中, 还有实例继承,拷贝继承,组合继承,寄生组合继承等众多继承方式。在寄生组合继承中, 就很好的弥补了callapply无法继承原型属性和方法的缺陷,是最完美的继承方法。这里就不详细的展开论述,感兴趣的可以自行阅读《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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.