在说明JavaScript是一个面向对象的语言以前, 咱们来探讨一下面向对象的三大基本特征: 封装, 继承, 多态。javascript
封装html
把抽象出来的属性和对方法组合在一块儿, 且属性值被保护在内部, 只有经过特定的方法进行改变和读取称为封装
咱们以代码举例, 首先咱们构造一个Person
构造函数, 它有name
和id
两个属性, 并有一个sayHi
方法用于打招呼:前端
//定义Person构造函数 function Person(name, id) { this.name = name; this.id = id; } //在Person.prototype中加入方法 Person.prototype.sayHi = function() { console.log('你好, 我是' + this.name); }
如今咱们生成一个实例对象p1
, 并调用sayHi()
方法java
//实例化对象 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('对不起, 你没有权限查看密码'); } }
继承es6
可让某个类型的对象得到另外一个类型的对象的属性和方法称为继承
以刚才的Person
做为父类构造器, 咱们来新建一个子类构造器Student
, 这里咱们使用call()
方法实现继承web
function Student(name, id, subject) { //使用call实现父类继承 Person.call(this, name, id); //添加子类的属性 this.subject = subject; } let s1 = new Student('阿辉', 1234, '前端开发');
多态编程
同一操做做用于不一样的对象产生不一样的执行结果, 这称为多态
JavaScript中函数没有重载, 因此JavaScript中的多态是靠函数覆盖实现的。数组
一样以刚才的Person
构造函数为例, 咱们为Person
构造函数添加一个study
方法架构
function Person(name, id) { this.name = name; this.id = id; this.study = function() { console.log(name + '在学习'); } }
一样, 咱们新建一个Student
和Teacher
构造函数, 该构造函数继承Person
, 并也添加study
方法
function Student(subject) { this.subject = subject; this.study = function() { console.log(this.name + '在学习' + this.subject); } } Student.prototype = new Person('阿辉', 1234); Student.prototype.constructor = Student; function Teacher(subject) { this.subject = subject; this.study = function() { console.log(this.name + '为了教学而学习' + this.subject); } } Teacher.prototype = new Person("老夫子", 4567); Teacher.prototype.constructor = Teacher;
测试咱们新建一个函数doStudy
function doStudy(role) { if(role instanceof Person) { role.study(); } }
此时咱们分别实例化Student
和Teacher
, 并调用doStudy
方法
let student = new Student('前端开发'); let teacher = new Teacher('前端开发'); doStudy(student); //阿辉在学习前端开发 doStudy(teacher); //老夫子为了教学在学习前端开发
对于同一函数doStudy
, 因为参数的不一样, 致使不一样的调用结果,这就实现了多态.
JavaScript的面向对象
从上面的分析能够论证出, JavaScript是一门面向对象的语言, 由于它实现了面向对象的全部特性. 其实, 面向对象仅仅是一个概念或者一个编程思想而已, 它不该该依赖于某个语言存在, 好比Java采用面向对象思想构造其语言, 它实现了类, 继承, 派生, 多态, 接口等机制. 可是这些机制,只是实现面向对象的一种手段, 而非必须。换言之, 一门语言能够根据自身特性选择合适的方式来实现面向对象。 因为大多数程序员首先学习的是Java, C++等高级编程语言, 于是先入为主的接受了“类”这个面向对象实际方式,因此习惯性的用类式面向对象语言中的概念来判断该语言是不是面向对象的语言。这也是不少有其余编程语言经验的人在学习JavaScript对象时,感受到很困难的地方。
实际上, JavaScript是经过一种叫原型(prototype)的方式来实现面向对象编程的。下面咱们就来讨论一下基于类(class-basesd)的面向对象和基于原型(protoype-based)的面向对象这二者的差异。
基于类的面向对象
在基于类的面向对象语言中(好比Java和C++), 是构建在类(class)和实例(instance)上的。其中类定义了全部用于具备某一特征对象的属性。类是抽象的事物, 而不是其所描述的所有对象中的任何特定的个体。另外一方面, 一个实例是一个类的实例化,是其中的一个成员。
基于原型的面向对象
在基于原型的语言中(如JavaScript)并不存在这种区别:它只有对象!不管是构造函数(constructor),实例(instance),原型(prototype)自己都是对象。基于原型的语言具备所谓的原型对象的概念,新对象能够从中得到原始的属性。
因此,在JavaScript中有一个颇有意思的__proto__
属性(ES6如下是非标准属性)用于访问其原型对象, 你会发现,上面提到的构造函数,实例,原型自己都有__proto__
指向原型对象。其最后顺着原型链都会指向Object
这个构造函数,然而Object
的原型对象的原型是null
,不信, 你能够尝试一下Object.prototype.__proto__ === null
为true
。然而typeof null === 'object'
为true
。到这里, 我相信你应该就能明白为何JavaScript这类基于原型的语言中没有类和实例的区别, 而是万物皆对象!
差别总结
基于类的(Java) | 基于原型的(JavaScript) |
---|---|
类和实例是不一样的事物。 | 全部对象均为实例。 |
经过类定义来定义类;经过构造器方法来实例化类。 | 经过构造器函数来定义和建立一组对象。 |
经过 new 操做符建立单个对象。 | 相同 |
经过类定义来定义现存类的子类, 从而构建对象的层级结构 | 指定一个对象做为原型而且与构造函数一块儿构建对象的层级结构 |
遵循类连接继承属性 | 遵循原型链继承属性 |
类定义指定类的全部实例的全部属性。没法在运行时动态添加属性 | 构造器函数或原型指定初始的属性集。容许动态地向单个的对象或者整个对象集中添加或移除属性。 |
*这里的ES5并不特指ECMAScript 5, 而是表明ECMAScript 6 以前的ECMAScript!
在ES5中建立对象有两种方式, 第一种是使用对象字面量的方式, 第二种是使用构造函数的方式。该两种方法在特定的使用场景分别有其优势和缺点, 下面咱们来分别介绍这两种建立对象的方式。
咱们经过对象字面量的方式建立两个student
对象,分别是student1
和student2
。
var student1 = { name: '阿辉', age: 22, subject: '前端开发' }; var student2 = { name: '阿傻', age: 22, subject: '大数据开发' };
上面的代码就是使用对象字面量的方式建立实例对象, 使用对象字面量的方式在建立单一简单对象的时候是很是方便的。可是,它也有其缺点:
name
,age
,subject
属性,写起来特别的麻烦student1
和student2
之间有什么联系。为了解决以上两个问题, JavaScript提供了构造函数建立对象的方式。
构造函数就其实就是一个普通的函数,当对构造函数使用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');
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
从新指向其构造函数。
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, '小学义务教育课程', '北大附小');
JavaScript中的继承方式不只仅只有上面提到的几种方法, 在《JavaScript高级程序设计》中, 还有实例继承,拷贝继承,组合继承,寄生组合继承等众多继承方式。在寄生组合继承中, 就很好的弥补了call
和apply
没法继承原型属性和方法的缺陷,是最完美的继承方法。这里就不详细的展开论述,感兴趣的能够自行阅读《JavaScript高级程序设计》。
基于原型的继承方式,虽然实现了代码复用,可是行文松散且不够流畅,可阅读性差,不利于实现扩展和对源代码进行有效的组织管理。不得不认可,基于类的继承方式在语言实现上更健壮,且在构建可服用代码和组织架构程序方面具备明显的优点。因此,ES6中提供了基于类class
的语法。但class
本质上是ES6提供的一颗语法糖,正如咱们前面提到的,JavaScript是一门基于原型的面向对象语言。
咱们使用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
方法,就能推断出上面的结论。
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的基本要求之一!