08月07, 2022

JS进阶(1)--对象(5)--构造函数

构造函数

构造函数创建对象

在前面的学习中,我们知道了在 JavaScript 中,没有类的概念,对象的产生是通过原型对象而来的。不过,JavaScript 能够模拟出面向对象的编程风格,使用函数来模拟其他面向对象语言中的类。

用于实例化对象的函数,我们将其称之为构造函数 不过,构造函数的实质也就是一个函数而已。所以,为了区分普通函数和构造函数,有一个不成文的规定,那就是构造函数的函数名首字母大写。

下面的例子演示了在 JavaScript 中如何书写一个构造函数:

const Computer = function(name,price){
    this.name = name;
    this.price = price;
}
Computer.prototype.showSth = function(){
    console.log(`这是一台${this.name}电脑`);
}

这里,我们创建了一个Computer的构造函数,该构造函数拥有两个成员属性,分别是nameprice,有一个成员方法,是showSth()。但是可以看到,这里的成员方法showSth()是书写在Computer类的 prototype 上面的。

在前面我们也已经介绍过了,prototype将会指向原型对象。之所以将方法添加在原型对象上面,是因为对于每个对象来讲方法体都是相同的,没有必要每个对象都像属性那样单独拥有一份,所以将方法添加至原型上面,这样就达到了方法共享的效果。

好了,既然这里我们的构造函数已经书写好了,那么接下来就可以从该构造函数里面来实例化对象出来了。通过new运算符,可以从构造函数中实例化出来一个对象。

换句话说,当使用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造函数里面的 this 就指向返回的这个对象。示例如下:

const Computer = function(name,price){
    this.name = name;
    this.price = price;
}
Computer.prototype.showSth = function(){
    console.log(this); // 打印出 this 所指向的对象
    console.log(`这是一台${this.name}电脑`);
}
const apple = new Computer("苹果",12000);
console.log(apple.name);// 苹果
console.log(apple.price);// 12000
apple.showSth();// Computer { name: '苹果', price: 12000 } 这是一台苹果电脑
const asus = new Computer("华硕",5000);
console.log(asus.name);// 华硕
console.log(asus.price);// 5000
asus.showSth();// Computer { name: '华硕', price: 5000 } 这是一台华硕电脑

这里,我们分别实例化出来了 2 个对象 apple 和 asus。这 2 个对象拥有各自的属性值。

注:当我们使用 new 运算符从构造函数实例化对象时,实际上在 JavaScript 引擎内部,也是先克隆了Object.prototype对象,然后再进行一些其他的额外操作。

[扩展]构造函数显式返回内容

在上面有讲过,构造函数的实质就是函数,所以函数有的特性,构造函数也应该拥有。在函数中,我们可以通过 return 关键字返回数据,在构造函数中仍然如此。

不过,需要注意的是,如果构造函数显式地返回了一个 object 类型的对象,那么此次运算结果最终会是返回这个对象,而不是我们之前所期待的 this。

这里我通过下面的 2 段代码来演示构造函数显式返回 object 类型对象的区别,如下:

正常情况,构造函数没有返回 object 类型对象,this 指向实例化出来对象

const Computer = function (name, price) {
    this.name = name;
    this.price = price;
}
Computer.prototype.showSth = function () {
    console.log(this); // 打印出 this 所指向的对象
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
apple.showSth(); // Computer { name: '苹果', price: 12000 }

如果构造函数显式的返回一个 object 类型的对象,那么最终使用的就是手动返回的这个对象

const Computer = function (name, price) {
    this.name = name;
    this.price = price;
    // 显式返回一个object类型对象
    return {
        name: "yingside",
        showSth: function () {
            console.log(this); // 打印出 this 所指向的对象
        }
    }
}
Computer.prototype.showSth = function () {
    console.log(this); // 打印出 this 所指向的对象
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // yingside
apple.showSth(); // { name: 'yingside', showSth: [Function: showSth] }

通过上面的例子,我们可以看到,如果构造器函数不显式返回任何数据,this 则就指向实例化出来的对象,而如果显式的返回一个 object 类型对象,那么最终使用的就是手动返回的那个对象。

为什么我这里一直强调是 object 类型对象呢?

实际上,如果返回的只是普通数据,那么 this 也是指向实例化出来的对象,如下:

返回 object 对象的情况:

const Student = function () {
    this.name = '张三';
    return { //对象
        name: '李四'
    };
}
const obj = new Student();
console.log(obj.name); // 李四

返回普通数据的情况:

const Student = function () {
    this.name = '张三';
    return '李四';
}
const obj = new Student();
console.log(obj.name); // 张三

ECMAScript 6 中类的声明

从 ECMAScript 6 开始,JavaScript 已经开始越来越贴近其他的高级语言了。在 ECMAScript 6 中有了类这个概念,使用 class 关键字来声明一个类来,然后从类里面实例化对象。

传统的构造函数的问题

  1. 属性和原型方法定义分离,降低了可读性
  2. 原型成员可以被枚举
  3. 默认情况下,构造函数仍然可以被当作普通函数使用

类的特点

  1. 类声明不会被提升,与 let 和 const 一样,存在暂时性死区
  2. 类中的所有代码均在严格模式下执行
  3. 类的所有方法都是不可枚举的
  4. 类的所有方法都无法被当作构造函数使用
  5. 类的构造器必须使用 new 来调用

注:虽然有了 class 关键字,但这只是一个语法糖,语法背后对象的创建,还是使用的是原型的方式。

具体示例如下:

class Computer {
    //构造器
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
    //原型方法
    showSth() {
        console.log(`这是一台${this.name}电脑`);
    }
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
console.log(apple.price); // 12000
apple.showSth(); // 这是一台苹果电脑

这里,Computer 类中有 2 个成员属性,分别是 name 和 price,有一个成员方法为showSth(),值得一提的是,该成员方法就是挂在原型对象上的,证明如下:

class Computer {
    //构造器
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
    //原型方法
    showSth() {
        console.log(`这是一台${this.name}电脑`);
    }
}
const apple = new Computer("苹果", 12000);
console.log(apple.name); // 苹果
console.log(apple.price); // 12000
apple.showSth(); // 这是一台苹果电脑
Computer.prototype.showSth(); // 这是一台 undefined 电脑

可以看到,Computer.prototype 指向的是实例对象的原型对象,这里同样成功调用到了showSth()方法。

静态方法

所谓静态方法,又被称之为类方法。顾名思义,就是通过类来调用的方法。静态方法的好处在于不需要实例化对象,直接通过类就能够进行方法的调用

在 ECMAScript 6 中要创建静态方法,可以在方法前面添加关键字 static,如下:

class Computer {
    // 构造器
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
    // 原型方法
    showSth() {
        console.log(`这是一台${this.name}电脑`);
    }
    // 静态方法
    static comStruct() {
        console.log("电脑由显示器,主机,键鼠组成");
    }
}
Computer.comStruct(); // 电脑由显示器,主机,键鼠组成

如果书写的是构造函数,也有办法来模拟静态方法,直接将方法挂在构造函数上即可,如下:

const Computer = function (name, price) {
    this.name = name;
    this.price = price;
}
Computer.prototype.showSth = function () {
    console.log(`这是一台${this.name}电脑`);
}
// 静态方法 直接通过 Computer 这个构造函数来调用
Computer.comStruct = function () {
    console.log("电脑由显示器,主机,键鼠组成");
}
Computer.comStruct(); // 电脑由显示器,主机,键鼠组成

类的继承

如果两个类A和B,如果可以描述为:B 是 A,则,A和B形成继承关系

如果B是A,则:

  1. B继承自A
  2. A派生B
  3. B是A的子类
  4. A是B的父类

如果A是B的父类,则B会自动拥有A中的所有实例成员。

新的关键字:

  • extends:继承,用于类的定义
  • super
    • 直接当作函数调用,表示父类构造函数
    • 如果当作对象使用,则表示父类的原型

注意:ES6要求,如果定义了constructor,并且该类是子类,则必须在constructor的第一行手动调用父类的构造函数

如果子类不写constructor,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用父类构造器

class Animal {
  constructor(type, name, age, sex) {
    if (new.target === Animal) {
      throw new TypeError("你不能直接创建Animal的对象,应该通过子类创建");
    }
    this.type = type;
    this.name = name;
    this.age = age;
    this.sex = sex;
  }

  print() {
    console.log(`【种类】:${this.type}`);
    console.log(`【名字】:${this.name}`);
    console.log(`【年龄】:${this.age}`);
    console.log(`【性别】:${this.sex}`);
  }

  jiao() {
    throw new Error("动物怎么叫的?");
  }
}

class Dog extends Animal {
  constructor(name, age, sex, loves) {
    super("犬类", name, age, sex);
    // 子类特有的属性
    this.loves = loves;
  }

  print() {
    //调用父类的print
    super.print();
    //自己特有的代码
    console.log(`【爱好】:${this.loves}`);
  }

  //同名方法,会覆盖父类
  jiao() {
    console.log("旺旺!");
  }
}

const a = new Dog("旺财", 3, "公");
a.print();
// a.jiao();

[扩展] 构造函数实现继承

其实用原来构造函数的方式也是能够实现继承的,大家可以了解一下,使用函数的call方法就能达到继承的效果

function User(username, age) { 
  this.username = username;
  this.age = age;
}
function VIPUser(username, age, type) { 
  User.call(this, username, age);
  this.type = type;
}

VIPUser.prototype = Object.create(User.prototype);

Object.defineProperty(VIPUser.prototype, "constructor", {
  enumerable: false,
  configurable: true,
  writable: true,
  value:VIPUser
})

let v = new VIPUser("aa", 20, "白金");

console.log(v);
console.log(v.__proto__.__proto__ === User.prototype);
console.log(v.__proto__);

将创建对象的函数封装:

// 工具函数
function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.create(SuperType.prototype)

  Object.defineProperty(SubType.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}

本文链接:http://www.yanhongzhi.com/post/js_ap_5.html

-- EOF --

Comments