首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。
许久已经没有写东西了,由于杂七杂八的缘由最近一直没有抽出时间来把写做坚持下来,感受和跑步同样,一旦松懈下来就很难再次捡起来。最近一直想从新静下心来写点什么,选题又成为一个让我头疼的问题,最近工做中偶尔会对JavaScript继承的问题有时候会感受恍惚,意识到不少知识即便是很基础,也须要常常的回顾和练习,不然即便再熟悉的东西也会常常让你感到陌生,因此就选择这么一篇很是基础的文章做为今年的开始吧。
javascript
JavaScript不像Java语言自己就具备类的概念,JavaScript做为一门基于原型(ProtoType
)的语言,(推荐我以前写的我所认识的JavaScript做用域链和原型链),时至今日,仍然有不少人不建议在JavaScript中大量使用面对对象的特性。但就目前而言,不少前端框架,例如React都有基于类的概念。首先明确一点,类存在的目的就是为了生成对象,而在JavaScript生成对象的过程并不不像其余语言那么繁琐,咱们能够经过对象字面量语法轻松的建立一个对象:前端
var person = { name: "MrErHu", sayName: function(){ alert(this.name); } };
一切看起来是这样的完美,可是当咱们但愿建立无数个类似的对象时,咱们就会发现对象字面量的方法就不能知足了,固然聪明的你确定会想到采用工厂模式去建立一系列的对象:
java
function createObject(name){ return { "name": name, "sayName": function(){ alert(this.name); } } }
可是这样方式有一个显著的问题,咱们经过工厂模式生成的各个对象之间并无联系,无法识别对象的类型,这时候就出现了构造函数。在JavaScript中构造函数和普通的函数没有任何的区别,仅仅是构造函数是经过new
操做符调用的。
git
function Person(name, age, job){ this.name = name; this.sayName = function(){ alert(this.name); }; } var obj = new Person(); obj.sayName();
咱们知道new
操做符会作如下四个步骤的操做:
github
[[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引擎这种问题能够忽略。对于构造函数的原型修改,处理上述的方式,可能还存在:
前端框架
Person.prototype.sayName = function(){ alert(this.name); }
咱们知道函数的原型中的constructor
属性是执行函数自己,若是你是将原来的原型替换成新的对象而且constructor
对你又比较重要记得手动添加,所以第一种并不许确,由于constructor
是不可枚举的,所以更准确的写法应该是:app
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