构造函数
构造函数创建对象
在前面的学习中,我们知道了在 JavaScript 中,没有类的概念,对象的产生是通过原型对象而来的。不过,JavaScript 能够模拟出面向对象的编程风格,使用函数来模拟其他面向对象语言中的类。
用于实例化对象的函数,我们将其称之为构造函数 不过,构造函数的实质也就是一个函数而已。所以,为了区分普通函数和构造函数,有一个不成文的规定,那就是构造函数的函数名首字母大写。
下面的例子演示了在 JavaScript 中如何书写一个构造函数:
const Computer = function(name,price){
this.name = name;
this.price = price;
}
Computer.prototype.showSth = function(){
console.log(`这是一台${this.name}电脑`);
}
这里,我们创建了一个Computer
的构造函数,该构造函数拥有两个成员属性,分别是name
和price
,有一个成员方法,是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 关键字来声明一个类来,然后从类里面实例化对象。
传统的构造函数的问题
- 属性和原型方法定义分离,降低了可读性
- 原型成员可以被枚举
- 默认情况下,构造函数仍然可以被当作普通函数使用
类的特点
- 类声明不会被提升,与 let 和 const 一样,存在暂时性死区
- 类中的所有代码均在严格模式下执行
- 类的所有方法都是不可枚举的
- 类的所有方法都无法被当作构造函数使用
- 类的构造器必须使用 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,则:
- B继承自A
- A派生B
- B是A的子类
- 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
})
}
Comments