JavaScript基础: 类与继承
2018-03-26 00:00
295 查看
JavaScript基础: 类与继承
类
JavaScript不像Java语言本身就具有类的概念,JavaScript作为一门基于原型(ProtoType)的语言,(推荐我之前写的我所认识的JavaScript作用域链和原型链),时至今日,仍然有很多人不建议在JavaScript中大量使用面对对象的特性。但就目前而言,很多前端框架,例如React都有基于类的概念。首先明确一点,类存在的目的就是为了生成对象,而在JavaScript生成对象的过程并不不像其他语言那么繁琐,我们可以通过对象字面量语法轻松的创建一个对象:
var person = { name: "MrErHu", sayName: function(){ alert(this.name); } };
一切看起来是这样的完美,但是当我们希望创建无数个相似的对象时,我们就会发现对象字面量的方法就不能满足了,当然聪明的你肯定会想到采用工厂模式去创建一系列的对象:
function createObject(name){ return { "name": name, "sayName": function(){ alert(this.name); } } }
但是这样方式有一个显著的问题,我们通过工厂模式生成的各个对象之间并没有联系,没法识别对象的类型,这时候就出现了构造函数。在JavaScript中构造函数和普通的函数没有任何的区别,仅仅是构造函数是通过
new操作符调用的。
function Person(name, age, job){ this.name = name; this.sayName = function(){ alert(this.name); }; } var obj = new Person(); obj.sayName();
我们知道
new操作符会做以下四个步骤的操作:
创建一个全新的对象
新对象内部属性
[[Prototype]](非正式属性
__proto__)连接到构造函数的原型
构造函数的
this会绑定新的对象
如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象
这样我们通过构造函数的方式生成的对象就可以进行类型判断。但是单纯的构造函数模式会存在一个问题,就是每个对象的方法都是相互独立的,而函数本质上就是一种对象,因此就会造成大量的内存浪费。回顾
new操作符的第三个步骤,我们新生成对象的内部属性
[[Prototype]]会连接到构造函数的原型上,因此利用这个特性,我们可以混合构造函数模式和原型模式,解决上面的问题。
function Person(name, age, job){ this.name = name; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var obj = new Person(); obj.sayName();
我们通过将
sayName函数放到构造函数的原型中,这样生成的对象在使用
sayName函数通过查找原型链就可以找到对应的方法,所有对象共用一个方法就解决了上述问题,即使你可能认为原型链查找可能会耽误一点时间,实际上对于现在的JavaScript引擎这种问题可以忽略。对于构造函数的原型修改,处理上述的方式,可能还存在:
Pe 3ff0 rson.prototype.sayName = function(){ alert(this.name); }
我们知道函数的原型中的
constructor属性是执行函数本身,如果你是将原来的原型替换成新的对象并且
constructor对你又比较重要记得手动添加,因此第一种并不准确,因为
constructor是不可枚举的,因此更准确的写法应该是:
Object.defineProperty(Person, "constructor", { configurable: false, enumerable: false, writable: true, value: Person });
到现在为止,我们会觉得在JavaScript中创建个类也太麻烦了,其实远远不止如此,比如我们创建的类可能会被直接调用,造成全局环境的污染,比如:
Person('MrErHu'); console.log(window.name); //MrErHu
不过我们迎来了ES6的时代,事情正在其变化,ES6为我们在JavaScript中实现了类的概念,上面的的代码都可以用简介的类(class)实现。
class Person { constructor(name){ this.name = name; } sayName(){ alert(this.name); } }
通过上面我们就定义了一个类,使用的时候同之前一样:
let person = new Person('MrErHu'); person.sayName(); //MrErHu
我们可以看到,类中的
constructor函数负担起了之前的构造函数的功能,类中的实例属性都可以在这里初始化。类的方法
sayName相当于之前我们定义在构造函数的原型上。其实在ES6中类仅仅只是函数的语法糖:
typeof Person //"function"
相比于上面自己创建的类方式,ES6中的类有几个方面是与我们自定义的类不相同的。首先类是不存在变量提升的,因此不能先使用后定义:
let person = new Person('MrErHu') class Person { //...... }
上面的使用方式是错误的。因此类更像一个函数表达式。
其次,类声明中的所有代码都是自动运行在严格模式下,并且不能让类脱离严格模式。相当于类声明中的所有代码都运行在"use strict"中。
再者,类中的所有方法都是都是不可枚举的。
最后,类是不能直接调用的,必须通过
new操作符调用。其实对于函数有内部属性
[[Constructor]]和
[[Call]],当然这两个方法我们在外部是没法访问到的,仅存在于JavaScript引擎。当我们直接调用函数时,其实就是调用了内部属性
[[Call]],所做的就是直接执行了函数体。当我们通过
new操作符调用时,其实就是调用了内部属性
[[Constructor]],所做的就是创建新的实例对象,并在实例对象上执行函数(绑定
this),最后返回新的实例对象。因为类中不含有内部属性
[[Call]],因此是没法直接调用的。顺便可以提一句ES6中的元属性 new.target
所谓的元属性指的就是非对象的属性,可以提供给我们一些补充信息。
new.target就是其中一个元属性,当调用的是
[[Constructor]]属性时,
new.target就是
new操作符的目标,如果调用的是
[[Call]]属性,
new.target就是
undefined。其实这个属性是非常有用的,比如我们可以定义一个仅可以通过
new操作符调用的函数:
function Person(){ if(new.target === undefined){ throw('该函数必须通过new操作符调用'); } }
或者我们可以用JavaScript创建一个类似于C++中的虚函数的函数:
class Person { constructor() { if (new.target === Person) { throw new Error('本类不能实例化'); } } }
继承
在没有ES6的时代,想要实现继承是一个不小的工作。一方面我们要在派生类中创建父类的属性,另一方面我们需要继承父类的方法,例如下面的实现方法:function Rectangle(width, height){ this.width = width; this.height = height; } Rectangle.prototype.getArea = function(){ return this.width * this.height; } function Square(length){ Rectangle.call(this, length, length); } Square.prototype = Object.create(Rectangle.prototype, { constructor: { value: Square, enumerable: false, writable: false, configurable: false } }); var square = new Square(3); console.log(square.getArea()); console.log(square instanceof Square); console.log(square instanceof Rectangle);
首先子类
Square为了创建父类
Rectangle的属性,我们在
Square函数中以
Rectangle.call(this, length, length)的方式进行了调用,其目的就是在子类中创建父类的属性,为了继承父类的方法,我们给
Square赋值了新的原型。除了通过
Object.create方式,你应该也见过以下方式:
Square.prototype = new Rectangle(); Object.defineProperty(Square.prototype, "constructor", { value: Square, enumerable: false, writable: false, configurable: false });
Object.create是ES5新增的方法,用于创建一个新对象。被创建的对象会继承另一个对象的原型,在创建新对象时还可以指定一些属性。
Object.create指定属性的方式与
Object.defineProperty相同,都是采用属性描述符的方式。因此可以看出,通过
Object.create与
new方式实现的继承其本质上并没有什么区别。 但是ES6可以大大简化继承的步骤:
class Rectangle{ constructor(width, height){ this.width = width; this.height = height; } getArea(){ return this.width * this.height; } } class Square extends Rectangle{ construct(length){ super(length, length); } }
我们可以看到通过ES6的方式实现类的继承是非常容易的。
Square的构造函数中调用
super其目的就是调用父类的构造函数。当然调用
super函数并不是必须的,如果你默认缺省了构造函数,则会自动调用
super函数,并传入所有的参数。 不仅如此,ES6的类继承赋予了更多新的特性,首先
extends可以继承任何类型的表达式,只要该表达式最终返回的是一个可继承的函数(也就是讲
extends可以继承具有
[[Constructor]]的内部属性的函数,比如
null和生成器函数、箭头函数都不具有该属性,因此不可以被继承)。比如:
class A{} class B{} function getParentClass(type){ if(//...){ return A; } if(//...){ return B; } } class C extends getParentClass(//...){ }
可以看到我们通过上面的代码实现了动态继承,可以根据不同的判断条件继承不同的类。 ES6的继承与ES5实现的类继承,还有一点不同。ES5是先创建子类的实例,然后在子类实例的基础上创建父类的属性。而ES6正好是相反的,是先创建父类的实例,然后在父类实例的基础上扩展子类属性。利用这个属性我们可以做到一些ES5无法实现的功能:继承原生对象。
function MyArray() { Array.apply(this, arguments); } MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true, enumerable: true } }); var colors = new MyArray(); colors[0] = "red"; colors.length // 0 colors.length = 0; colors[0] // "red"
可以看到,继承自原生对象
Array的
MyArray的实例中的
length并不能如同原生
Array类的实例 一样可以动态反应数组中元素数量或者通过改变
length属性从而改变数组中的数据。究其原因就是因为传统方式实现的数组继承是先创建子类,然后在子类基础上扩展父类的属性和方法,所以并没有继承的相关方法,但ES6却可以轻松实现这一点:
class MyArray extends Array { constructor(...args) { super(...args); } } var arr = new MyArray(); arr[0] = 12; arr.length // 1 arr.length = 0; arr[0] // undefined
我们可以看见通过
extends实现的
MyArray类创建的数组就可以同原生数组一样,使用
length属性反应数组变化和改变数组元素。不仅如此,在ES6中,我们可以使用
Symbol.species属性使得当我们继承原生对象时,改变继承自原生对象的方法的返回实例类型。例如,
Array.prototype.slice本来返回的是
Array类型的实例,通过设置
Symbol.species属性,我们可以让其返回自定义的对象类型:
class MyArray extends Array { static get [Symbol.species](){ return MyArray; } constructor(...args) { super(...args); } } let items = new MyArray(1,2,3,4); subitems = items.slice(1,3); subitems instanceof MyArray; // true
最后需要注意的一点,
extends实现的继承方式可以继承父类的静态成员函数,例如:
class Rectangle{ // ...... static create(width, height){ return new Rectangle(width, height); } } class Square extends Rectangle{ //...... } let rect = Square.create(3,4); rect instanceof Square; // true
相关文章推荐
- JavaScript中的原型继承基础学习教程
- JavaScript基础之继承
- JavaScript继承基础讲解(原型链、借用构造函数、混合模式、原型式继承、寄生式继承、寄生组合式继承)
- javascript 对象基础 继承机制实例【对象冒充】
- javascript基础:对象的继承、添加对象方法
- JavaScript中的原型继承基础学习教程
- [基础]Javascript中的继承示例代码
- javascript基础学习三:原型继承
- JavaScript学习:基础继承机制
- [基础]Javascript中的继承示例代码
- JavaScript原型继承之基础机制分析
- 【 js 基础 】【读书笔记】Javascript “继承”
- javascript 面向对象编程基础:继承
- 讲述Javascript 实现继承的方式(基础知识)
- Javascript基础知识盲点总结——继承
- JavaScript基础——面向对象的程序设计(二)继承
- Javascript OO 基础概念(三)–原型继承
- [导入]JavaScript基础之继承(二)
- javascript基础:prototype对象的继承
- JavaScript基础之继承1