javascript中的类,实例化,继承

2,212 阅读7分钟

在刚接触js的时候,我对于类这个概念是十分模糊的。但是后面慢慢接触多了,也明白了类的作用。写一点关于ES5中继承与实例化的理解,方便复习。

什么是类:

描述具有相同属性或者方法的对象的集合。

也就是说类是****具有相同属性或者方法的对象的抽象。

ES5中定义一个类

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.showPos = function () {
  return {x:this.x,y:this.y}
};

var p = new Point(1, 2);

类本身指向其构造函数。

Point.prototype.constructor == Point
// true

类的属性分为:类本身的属性,类的构造函数的属性,类的原型对象的属性。类的方法也是有以上三种。这三个东西对类来说都很重要,区分清楚对super的理解会很有帮助。

实例化的时候实例能获取到类的构造函数与原型对象上的属性与方法,但是不能获取类本身的属性和方法。类本身的属性与方法需要去类上去拿到,而不是去实例上拿到。

function Point(x, y) {
  // 类的构造函数的属性/方法
  this.x = x;
  this.y = y;
}
  // 类的原型对象的属性/方法
Point.prototype.showPos = function () {
  return {x:this.x,y:this.y}
};
 // 类本身的属性
Point.staticX = 110 var p = new Point(1, 2);
p.staticX; // undefined p.constructor.staticX; // 110
 p.__proto__.constructor.staticX // 110

 // 实例的构造函数是类本身,实例的原型指向类的原型对象,类的原型对象的构造函数就是类本身
 p.constructor == p.__proto__.constructor // true 

类本身的属性/方法在ES6中用static关键字去定义:

class Point {  static staticX = 110;}

类本身的方法叫静态方法,类本身的属性叫静态属性。

大家熟悉的Object对象,就具有静态属性和静态方法。

当new Object时获得Object的属性与方法,也可以直接调用Object的静态方法,如

Object.assign

但是Object的实例是没有 assign 方法的,js中一切对象都继承自Object,但是却没有assign方法就是这个道理。

类可以被实例化,可以被继承,自己对于实例化与继承的理解:

什么是实例化:

将抽象的类转为具体的对象。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.showPos = function () {
  return {x:this.x,y:this.y}
};

var p = new Point(1, 2);

在这里创建了一个Point类,p指向了Point的实例。

那么实例是如何获取到类的方法与属性的?

这里就要提到原型和原型对象两个概念。

原型对象-prototype:无论什么时候,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。这里需要说明的是只有函数才会有原型对象,对象是没有的,比如上面代码中的p是一个实例--是一个对象,所以p是没有原型对象的。

那么实例是如何得到类的方法以及属性的呢?

首先就是new干了什么:

1.创建一个新对象。

2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

3.执行构造函数中的代码。(为这个对象添加属性)。

4.返回这个对象。

以上是《js高级程序设计》中的描述,从描述上来看,我们只能看出实例获得了类的构造函数内的属性与方法,但是看不出类的原型对象上的方法是如何被获取的。以上的描述相当于:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.showPos = function () {
  return {x:this.x,y:this.y}
};

var p = new Point(1, 2);
p.__proto__ = null;
p.showPos;//undefined
p.x;//1

如果只是添加属性,实例是获取不到类的原型对象上的属性与方法的,只能获取到类的构造函数上的属性与方法。

所以如果要获得类的原型对象上的方法,其实还需要让实例与类再次建立一个联系,这个联系是为了让实例获取类的原型对象上的方法。

当调用类创建一个新实例后,该实例内部将包含一个指针[[prototype]](内部属性),指向类的原型对象。也就是说每当实例化一个对象时都会给实例添加一个名为[[prototype]]的属性,这个属性指向的就是类的原型对象。需要注意的是[[prototype]]指向的是类的原型对象,不是类的构造函数本身,也不是类的构造函数。所以[[prototype]]是获取不到类本身与类的构造函数内的属性与方法的。这个点在ES6中使用super时是个很重要的点。

模拟一下实例获取类的原型对象方法的过程:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.showPos = function () {
  return {x:this.x,y:this.y}
};
Point.staticX = 110

var p = {};
p.__proto__ = Point.prototype;
p.x // undefined
p.staticX // undefined

什么时候可以用new 操作符?

在ECMA官方规定中可以使用new的一个条件是 new 操作符后面的东西的 isConstructor 为true,isConstructor为true的条件是:这个东西是一个Object(函数也是对象)且具有内部槽属性 [[Construct]],什么时候对象具有 [[Construct]]呢?对象是一个函数。

ECMA官方有这么一段话:

创建对象,通过new或super运算符调用。内部方法的第一个参数是包含运算符参数的列表。第二个参数是新运算符最初应用到的对象。实现此内部方法的对象称为构造函数。函数对象不一定是构造函数,并且此类非构造函数函数对象没有[[Construct]]内部方法。

不是构造函数的函数对象:箭头函数

为了加深理解,打个比方:类是一份镶金的图纸,图纸里记录了某个器械的长宽与全部功能点,实例化是根据图纸造出来一个真正的器械。其中需要被实例获取的东西是器械的长宽(类的属性),以及器械的功能(类的原型对象上的属性),而图纸是否镶金边(类本身的属性)实例是不太关心的,虽然也可以通过实例的__proto__找到类的原型对象继而找到类的构造函数去获取这个属性。

继承:

function Super(name) {    
  this.name = name;
}
Super.prototype.sayName = function() {    return 'xiaoming';};
 // 第一步
function Sub(name, value) {   
 Super.call(this, name);    
 this.prop = value;
}

// 第二步
Sub.prototype = Object.create(Super.prototype);
// 另一种写法
// Sub.prototype = new Super(); // 问题在会调用两次构造函数,子类的构造函数内和原型对象上会存在两份属性
// 还有一种写法
// Sub.prototype = Super.prototype // 问题在子类和父类共享了原型对象,子类和父类在原型链上增删更改函数或者属性会互相影响

// 第三步 
// 这时由于子类的prototype在上一步被指向了父类的实例,如果不改变其constructor,Sub.prototype.constructor == 实例的constructor == Super
// 而子类继承了父类之后,我们可能会直接用子类的实例给子类添加属性和方法,如果不指回去会添加到Super上,这不在预期内,所以这里必须指回子类本身
Sub.prototype.constructor = Sub; 
Sub.prototype.method = '...';

第一步,Sub是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Super,就会让子类实例具有父类构造函数内的属性。

第二步,是让子类的原型对象指向父类的原型对象,这样子类就可以继承父类原型。

这是《js高级程序设计》中的寄生组合式继承,是目前来说公认最佳的继承方式。

另外一种写法是Sub.prototype等于一个父类实例,即借用构造函数与原型链实现继承。

Sub.prototype = new Super();

上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。

借用构造函数与原型链实现继承是最常用的方式,其与寄生组合式继承的区别书里写的很明白,贴一张魔爪图吧。书中172页。