es6之前,javascript本质上不能算是一门面向对象的编程语言,因为它对于封装、继承、多态这些面向对象语言的特点并没有在语言层面上提供原生的支持。但是,它引入了原型(prototype)的概念,可以让我们以另一种方式模仿类,并通过原型链的方式实现了父类子类之间共享属性的继承以及身份确认机制。
一、封装
Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为在 ES6 之前它的语法中都没有class(类)的概念。
那么,如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?
1、 生成实例对象的原始模式
假定我们把猫看成一个对象,它有”名字”和”颜色”两个属性。
1 |
|
这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。
2、 构造函数模式
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
1 |
|
这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。
1 |
|
Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。
1 |
|
构造函数方法很好用,但是存在一个浪费内存的问题。 对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
1 |
|
能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。
3、 Prototype模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。
1 |
|
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
1 |
|
4、 Prototype模式的验证方法
为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它。
1>、 isPrototypeOf()
这个方法用来判断,某个proptotype对象和某个实例之间的关系。
1 |
|
2>、 hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。
1 |
|
3>、 in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
1 |
|
in运算符还可以用来遍历某个对象的所有属性。
1 |
|
二、继承
有下面一个例子:
1 |
|
怎样才能使”猫”继承”动物”呢?有一下集中方法:
1、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
1 |
|
优点:
- 解决了原型链继承不能传参的问题
- 子类实例共享父类引用属性的问题
- 可以实现多继承(call可以指定不同的超类)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用
2、原型链继承
利用原型让一个引用类型继承另一个引用类型的属性和方法
1 |
|
我们创建了两个构造函数 Animal和 Cat,并且让 Cat的原型指向 Animal,Cat也就继承了 Animal原型对象中的方法。所以在创建 cat1实例的时候,实例本身也就具有了 Animal 中的方法,并且都处在它们的原型链中;
代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。但是,第二行又是什么意思呢?
任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。
这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
优点:
- 简单、易于实现
- 父类新增原型方法/原型属性,子类都能访问到
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
缺点:
- 无法实现多继承
- 想要为子类 SubType 添加原型方法,就必须在 new SuperType 之后添加(会覆盖)
- 来自原型对象的所有属性被所有实例共享(引用类型的值修改会反映在所有实例上面)
- 创建子类实例时,无法向父类构造函数传参
这种方案最大问题就是子类无法通过父类创建私有属性。比如每一个Cat都有一个species,我们在初始化每个Cat的时候要species都是’动物’,而无法通过想父类构造函数传参来改变它。所以,这种继承方式,实战中基本不用!
3、组合继承
伪经典继承(最常用的继承模式):将原型链和借用构造函数的技术组合到一起。使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承
1 |
|
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4、类式继承
利用一个空对象作为中介。F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
1 |
|
这个extend函数,就是YUI库如何实现继承的方法。
优点:
- 支持多继承(传入的对象不同)
- 不需要兴师动众的创建很多构造函数
缺点:
- 和原型链继承基本一致,效率较低,内存占用高(因为要拷贝父类的属性)
5、寄生式继承
创建一个仅用于封装继承过程的函数,在函数内部对这个对象进行改变,最后返回这个对象
1 |
|
优点:
- 支持多继承
缺点:
- 实例并不是父类的实例,只是子类的实例
- 不能实现复用(与构造函数相似)
- 实例之间会互相影响
6、寄生组合继承
借用构造函数来继承属性,通过原型链的混成形式来继承方法。通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
1 |
|
寄生组合继承的特点: 堪称完美,只是实现稍微复杂一点
三、多态
实现 Javascript ‘多态’最关键的是理解 arguments
,它是每个 function 都自带的一个系统变量,是用来存储函数的参数的一个数组。
1 |
|
- 本文作者: Alvin
- 本文链接: https://alvinyw.github.io/2018/05/10/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!