js 一直允许定义类。ES6新增了相关语法(包括class关键字)让创建类更容易。新语法创建的类和老式的类原理相同。js 的类和基于原型的继承机制与Java等语言中的类和继承机制有着本质区别。
类意味着一组对象从同一个原型对象继承属性。因此,原型对象是类的核心特征。
用工厂函数创建和初始化该类的新实例:
function range(from, to) {
let r = Object.create(range.methods);
r.from = from;
r.to = to;
return r;
}
range.methods = {
includes(x) {
return this.from <= x && x <= this.to;
},
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++)
yield x;
},
toString() {
return "(" + this.from + "..." + this.to + ")";
},
}
let r = range(1,3);
console.log(r.includes(2));
console.log(r + ""); // 调用的是toString()方法
console.log(r); // 打印对象本身,而非调用toString()方法
console.log([...r])
只有函数(不包括箭头函数、生成器函数和异步函数)对象才有prototype属性。
上面定义类的写法非习惯写法,因为它没有定义构造函数。构造函数要使用new关键字调用,会自动创建新对象,因此构造函数本身只需初始化新对象的状态。构造函数调用的关键在于构造函数的prototype属性将被用作新对象的原型。
function Range(from,to) {
this.from = from;
this.to = to;
}
// 原型属性名必须命名为prototype
Range.prototype = {
includes(x) {
return this.from <= x && x <= this.to;
},
*[Symbol.iterator]() {
for (let x = Math.ceil(this.from); x <= this.to; x++)
yield x;
},
toString() {
return "(" + this.from + "..." + this.to + ")";
}
}
let r = new Range(1,3);
console.log(r.includes(2));
console.log(r + "");
console.log(r)
console.log([...r])
当且仅当两个对象继承同一个原型对象时,它们才是同一个类的实例。instanceof 操作符用于检查某个对象是否是某个特定类的实例。
严格来讲,instanceof操作符并非检查对象是否通过某个构造函数初始化,而是检查对象是否继承了原型。(不一定是直接继承)
let prototype = {
}
function Person() {}
function Animal() {}
let p = new Person();
console.log( p instanceof Person); // true
console.log( p instanceof Animal); // false
Person.prototype = prototype
Animal.prototype = prototype
let p2 = new Person()
console.log( p2 instanceof Person); // true
console.log( p2 instanceof Animal); // true
prototype属性的值是一个对象,其有一个不可枚举的constructor属性,该属性的值就是prototype所属的函数对象:
let F = function() {};
let p = F.prototype;
let c = p.constructor;
c === F; //true
class关键字是在ES6才引入的。
使用class来定义类:
class Person {
constructor(age) {
this.age = age;
}
showAge() {
console.log(this.age);
}
}
let p = new Person(12);
p.showAge();
let Animal = class {
constructor(name) {
this.name = name
}
showName() {
console.log(this.name)
}
}
let a = new Animal("大象");
a.showName()
与函数声明不同,类声明不会“提升”(函数定义就像是会被提升到包含文件或包含函数顶部一样,而类声明不会)。
类声明除了语句到形式外,还有表达式的形式。不过这种形式并不常用:
let Persopn = class { constructor() {} }
在class体中,把static关键字放在方法声明前面可以定义静态方法。静态方法是作为构造函数而非原型的属性定义的。
class Person {
static hello() {
console.log("hello word")
}
}
Person.hello(); // hello word
let p = new Person();
p.hello(); // 报错,p.hello is not a function
静态函数必须通过构造函数而非实例调用。
在class 体内,可像对象字面量中一样定义获取方法和设置方法,唯一的区别是类体内的方法后面不加逗号。
class Person {
set age(val) {
if (val <= 0) throw new TypeError("年龄不能小于0")
this.age = val
}
}
let person = new Person();
person.age = -1
console.log(person.age)
ES6之前定义子类的方式是通过原型,ES6则通过extends定义子类。
function Person() {
console.log("Person的构造器")
}
Person.prototype = {
constructor: Person,
showType() {
console.log("Person")
}
}
function Man() {
console.log("Man的构造器")
}
Man.prototype = Object.create(Person.prototype)
let man = new Man();
man.showType()
console.log(man.constructor)
Man.prototype.constructor = Man
console.log(man.constructor)
只有在知道父类实现细节的前提下才可能这样定义子类,健壮的子类化机制应该允许类调用父类的方法和构造函数,在ES6之前,js中没有简单的方法做到这些。
class Person {
constructor() {
console.log("Person:" + new.target)
}
}
class Man extends Person {
constructor() {
super();
console.log(this)
console.log("Man")
}
}
class Woman extends Person {
constructor() {
super();
console.log("Woman")
}
}
let man = new Man();
let woman = new Woman()
注意事项:
1)使用extends关键字定义了一个类,则该类的构造函数必须使用super()调用父类构造器。
2)在通过super()调用父类构造器之前,不能在构造器中使用this关键字。
3)new.target引用的是被调用的构造函数。
在实际开发中,并不建议创建很多子类,建议通过“组合”的方式来替代继承。