es6语法特性进阶(7)


和大多数面向对象的语言(object-oriented programming language)不同,JavaScript 在诞生之初并不支持使用类和传统的类继承并作为主要的定义方式来创建相似或关联的对象。
这很令开发者困惑,而且在早于 ECMAScript 1 到 ECMAScript 5 这段时期,很多库都创建了一些实用工具(utility)来让 JavaScript 从表层上支持类。
尽管一些 JavaScript 开发者强烈主张该语言不需要类,但由于大量的库都对类做了实现,ECMAScript 6 也顺势将其引入。

ES5之前的模拟的类
在 ECMAScript 5 或更早的版本中,JavaScript 没有类。和类这个概念及行为最接近的是创建一个构造函数并在构造函数的原型上添加方法,这种实现也被称为自定义的类型创建,例如:

1
2
3
4
5
6
7
8
9
10
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
console.log(this.name);
};
let person = new PersonType("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true

前面的PersonType我们以前一直叫做构造函数,其实他就是一个类型,因为他确实表示了一种类型。

ES6中基本的类声明
在ES6直接借鉴其他语言,引入了类的概念。所以再实现上面那种模拟 的类就容易了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//class关键字必须是小写。 后面就是跟的类名
class PersonClass {
// 等效于 PersonType 构造函数。
constructor(name) { //这个表示类的构造函数。constuctor也是关键字必须小写。
this.name = name; //创建属性。 也叫当前类型的自有属性。
}
// 等效于 PersonType.prototype.sayName. 这里的sayName使用了我们前面的简写的方式。
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

1.自有属性:属性只出现在实例而不是原型上,而且只能由构造函数和方法来创建。在本例中,name 就是自有属性。我建议 尽可能的将所有自有属性创建在构造函数中,这样当查找属性时可以做到一目了然。
2.类声明只是上例中自定义类型的语法糖。PersonClass 声明实际上创建了一个行为和 constructor 方法相同的构造函数,这也是 typeof PersonClass 返回 “function” 的原因。sayName() 在本例中作为 PersonClass.prototype 的方法,和上个示例中 sayName() 和 PersonType.prototype 关系一致。这些相似度允许你混合使用自定义类型和类而不需要纠结使用方式。
虽然类和以前的使用构造函数+原型的方式很像,但是还是有一些不太相同的地方,而且要牢记

1.类声明和函数定义不同,类的声明是不会被提升的。类声明的行为和 let 比较相似,所以当执行流作用到类声明之前类会存在于暂存性死区(temporal dead zone)内。
2.类声明中的代码自动运行在严格模式下,同时没有任何办法可以手动切换到非严格模式。
3.所有的方法都是不可枚举的(non-enumerable),这和自定义类型相比是个显著的差异,因为后者需要使用 Object.defineProperty() 才能定义不可枚举的方法。
4.所有的方法都不能使用 new 来调用,因为它们没有内部方法 [[Construct]]。
5.不使用 new 来调用类构造函数会抛出错误。也就是 必须使用new 类() 的方式使用
6.试图在类的方法内部重写类名的行为会抛出错误。(因为在类的内部,类名是作为一个常量存在的)

匿名类表达式
函数有函数表达式,类也有类表达式。
类表达式的功能和前面的类的声明是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let PersonClass = class {
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("Nicholas");
person.sayName(); // 输出 "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

具名类表达式

1
2
3
4
5
6
7
8
9
10
11
let PersonClass = class PersonClass2{
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};

注意:具名类表达式中PersonClass2这个类名只能在类的内部访问到,在外面是访问不到的.

作为一等公民的类型
在JavaScript中,函数是作为一等公民存在的。(也叫一等函数)。

类也是一等公民。
类可以作为参数传递

1
2
3
4
5
6
7
8
9
10
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});
obj.sayHi(); // "Hi!"

立即调用类构造函数,创建单例

1
2
3
4
5
6
7
8
9
10
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}("Nicholas");
person.sayName(); // "Nicholas"

动态计算类成员的命名
类的成员,也可以像我们前面的对象的属性一样可以动态计算.( 使用[ ] 来计算)

1
2
3
4
5
6
7
8
9
10
11
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
}
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"

静态成员
在ES5中,我们可以直接给构造函数添加属性或方法来模拟静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
function PersonType(name) {
this.name = name;
}
// 静态方法。 直接添加到构造方法上。 (其实是把构造函数当做一个普通的对象来用。)
PersonType.create = function(name) {
return new PersonType(name);
};
// 实例方法
PersonType.prototype.sayName = function() {
console.log(this.name);
};
var person = PersonType.create("Nicholas");

在上面的create方法在其他语言中一般都是作为静态方法来使用的。
下面高能,请注意:
ECMAScript 6 的类通过在方法之前使用正式的 static 关键字简化了静态方法的创建。例如,下例中的类和上例相比是等效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PersonClass {
// 等效于 PersonType 构造函数
constructor(name) {
this.name = name;
}
// 等效于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
// 等效于 PersonType.create。
static create(name) {
return new PersonClass(name);
}
}
let person = PersonClass.create("Nicholas");

注意:静态成员通过实例对象不能访问,只能通过类名访问!!!
通过和ES5模拟静态方法的例子你应该知道为啥了吧

ES6中的继承
在ES6之前要完成继承,需要写很多的代码。看下面的继承的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Father(name) {
this.name = name;
}
Father.prototype.sayName = function () {
console.log(this.name);
}
function Son(name,age) {
Father.call(this, name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function () {
console.log(this.age);
}
var son1 = new Son("儿子", 20);
son1.sayAge(); //20
son1.sayName(); //儿子

继承的基本写法
如果在ES6通过类的方式完成继承就简单了很多。
需要用到一个新的关键字:extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Father{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
class Son extends Father{ //extents后面跟表示要继承的类型
constructor(name, age){
super(name); //相当于以前的:Father.call(this, name);
this.age = age;
}
//子类独有的方法
sayAge(){
console.log(this.age);
}
}
var son1 = new Son("李四", 30);
son1.sayAge();
son1.sayName();
console.log(son1 instanceof Son); // true
console.log(son1 instanceof Father); //true

这种继承方法,和我们前面提到的构造函数+原型的继承方式本质是一样的。但是写起来更简单,可读性也更好。
关于super的使用,有几点需要注意:
你只能在派生类中使用 super(),否则(没有使用 extends 的类或函数中使用)一个错误会被抛出。
你必须在构造函数的起始位置调用 super(),因为它会初始化 this。任何在 super() 之前访问 this 的行为都会造成错误。也即是说super()必须放在构造函数的首行。
在类构造函数中,唯一能避免调用 super() 的办法是返回一个对象。

在子类中屏蔽父类的方法

如果在子类中声明与父类中的同名的方法,则会覆盖父类的方法。(这种情况在其他语言中称之为 方法的覆写、重写 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Father{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
class Son extends Father{ //extents后面跟表示要继承的类型
constructor(name, age){
super(name); //相当于以前的:Father.call(this, name);
this.age = age;
}
//子类独有的方法
sayAge(){
console.log(this.age);
}
//子类中的方法会屏蔽到父类中的同名方法。
sayName(){
super.syaName(); //调用被覆盖的父类中的方法。
console.log("我是子类的方法,我屏蔽了父类:" + name);
}
}
var son1 = new Son("李四", 30);
son1.sayAge();
son1.sayName();

如果在子类中又确实需要调用父类中被覆盖的方法,可以通过super.方法()来完成。

注意:
如果是调用构造方法,则super不要加点,而且必须是在子类构造方法的第一行调用父类的构造方法
普通方法调用需要使用super.父类的方法() 来调用。

静态方法也可以继承

1
2
3
4
5
6
7
8
9
10
class Father{
static foo(){
console.log("我是父类的静态方法");
}
}
class Son extends Father{
}
Son.foo(); //子类也继承了父类的静态方法。 这种方式调用和直接通过父类名调用时一样的。

访客的ip和所在地址: 访问时间: 当前时间:

殖民 wechat
欢迎您扫一扫上面的微信公众号
打赏不在多少,请鼓励一下!