资讯专栏INFORMATION COLUMN

JavaScript基础: 类与继承

liuchengxu / 1398人阅读

摘要:类的方法相当于之前我们定义在构造函数的原型上。的构造函数中调用其目的就是调用父类的构造函数。是先创建子类的实例,然后在子类实例的基础上创建父类的属性。

前言

  首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。
  
  许久已经没有写东西了,因为杂七杂八的原因最近一直没有抽出时间来把写作坚持下来,感觉和跑步一样,一旦松懈下来就很难再次捡起来。最近一直想重新静下心来写点什么,选题又成为一个让我头疼的问题,最近工作中偶尔会对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引擎这种问题可以忽略。对于构造函数的原型修改,处理上述的方式,可能还存在:
  

Person.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.createnew方式实现的继承其本质上并没有什么区别。
  
  但是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"

  可以看到,继承自原生对象ArrayMyArray的实例中的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

  
  

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93677.html

相关文章

  • javascript类与继承

    摘要:的类与继承的类与一般的面向对象语言有很大的不同,类的标识是它的构造函数,下面先定义一个类显然我们可以看出这两个函数是不同的,虽然它们实现了相同的功能。利用构造函数来继承上面的方法子类显然无法继承父类的原型函数,这样不符合我们使用继承的目的。 javascript的类与继承 javascript的类与一般的面向对象语言有很大的不同,类的标识是它的构造函数,下面先定义一个类 var ...

    impig33 评论0 收藏0
  • JavaScript实现类与继承的方法(全面整理)

    摘要:定义类的种方法工厂方法构造函数方法原型方法大家可以看到这种方法有缺陷,类里属性的值都是在原型里给定的。组合使用构造函数和原型方法使用最广将构造函数方法和原型方法结合使用是目前最常用的定义类的方法。 JavaScript定义类的4种方法 工厂方法 function creatPerson(name, age) { var obj = new Object...

    Near_Li 评论0 收藏0
  • javascript---类与函数化

    摘要:函数用于指定对象的行为。关于属性只在构造器函数的原型上才有的属性并指向该构造器,改写了的原型对象默认是没有属性的。函数化工厂模式在伪类模式里,构造器函数不得不重复构造器已经完成的工作。 1.对象适合于收集和管理数据,容易形成树型结构。Javascript包括一个原型链特性,允许对象继承另一对象的属性。正确的使用它能减少对象的初始化时间和内存消耗。2.函数它们是javascript的基础...

    waruqi 评论0 收藏0
  • TS 类与接口

    摘要:二正文中与的区别接口只声明成员方法,不做实现。在中,我们可以采用全新的基于类继承的模式设计更优雅的语义化接口,这是因为中的可以继承动态构造的类,这一点和其他的静态声明类的编程语言不同。 一 前言 1.在typescript上遇到过实例化对象的两种写法:implement和extends。extends很明显就是ES6里面的类继承,那么implement又是做什么的呢?它和extends...

    garfileo 评论0 收藏0
  • Python方法(二) - 类与继承

    摘要:在类内部的方法中使用时。类的私有方法两个下划线开头,声明该方法为私有方法,不能在类地外部调用。先在本类中查找调用的方法,找不到才去基类中找。如果在继承元组中列了一个以上的类,那么它就被称作多重继承。 类定义 类对象:创建一个类之后,可以通过类名访问、改变其属性、方法 实例对象:类实例化后,可以使用其属性,可以动态的为实例对象添加属性(类似javascript)而不影响类对象。 类...

    DevWiki 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<