2.4JavaScript引用类型之类(Class)

类是一种“特殊的函数”。

class C {
}

typeof C;  // "function"

就像函数声明定义方式和函数表达式定义方式一样,类的定义方式也有两种:类声明定义方式和类表达式定义方式。

1.类声明

注意:在同一个作用域,类声明不可以重复定义,否则会报错。

class 类名 {
  //类体
}

2.类表达式

注意:在同一个作用域,类表达式不可以重复定义,否则会报错。

注意:命名类表达式的右侧依然是类表达式,不是类声明。

//命名类表达式
{ let | const } C = class C2 {
  //类体
};

//匿名类表达式
{ let | const } C = class {
  //类体
};

3.立即实例化的类表达式

具体可参考“立即调用的函数表达式”章节。

注意:在同一个作用域,立即实例化的类表达式可以重复定义,相当于两个互不干扰的构造函数调用。

首先看一下构造函数调用表达式的语法:

//无实参
new 类名();

//有实参
new 类名(实参);

因为类名是存储类定义的变量或常量,所以将类名直接替换为类定义也是合法的。

//命名类表达式
new class 类名 {
  //类体
}(实参);

//匿名类表达式
new class {
  //类体
}(实参);

4.类声明提升(Hoisting)

类声明、类表达式都不支持提升。

5.类名

类名是第一次跟类定义绑定的类名或变量或常量。

5.1类声明的类名

class C {}
console.log(C.name);               // C
console.log( (class C {}).name );  // C

注意:类声明的类名是变量,不是常量,因为在赋值后可以通过重新赋值改变其值。

class C {}
C = 2;
new C();         // 报错
console.log(C);  // 2

5.2类表达式的类名

//命名类表达式
let C = class C2 {};
console.log(C.name);                // C2
console.log( (class C2 {}).name );  // C2

//匿名类表达式
let C = class {};
console.log(C.name);             // C
console.log( (class {}).name );  // ""(空字符串)

注意:赋值表达式的右侧是命名类表达式时,并不意味着命名类表达式等同于类声明,命名类表达式的类名 C2 的作用域仅仅为类体,但不可以在类体外使用。

let C = class C2 {
  a = 1;
};
new C().a;   // 1 
new C2().a;  // 报错

6.类引用表达式

注意:实际上,类名可以为返回值为类定义的任何表达式。

类名;

7.类定义作为值

类定义(类声明、类表达式)是 Function 类型的实例,与其它引用类型的值(即对象)一样,类定义也可以作为值赋给变量或常量,保存为对象的属性或数组的元素,作为函数的实参或返回值,等等。

以下仅仅介绍部分使用场景。

7.1复制值

具体可参考“函数定义作为值”-“复制值”章节。

class C {
  constructor() {
    console.log(1);
  }
}

new C();  // 1
let B = C;
new B();  // 1

7.2回调类

具体可参考“函数定义作为值”-“回调函数”章节。

//作为实参
function callSomeClass(someClass) {
  return new someClass();
}

class C {
  constructor() {
    console.log(1);
  }
}

callSomeClass(C);  // 1
//作为返回值
function callSomeClass() {
  return class {
    constructor() {
      console.log(1);
    }
  };
}

let C = callSomeClass();
new C();  // 1

8.成员名

支持类的成员:静态字段、静态访问器属性、静态方法、实例字段、实例访问器属性、实例方法。

支持对象字面量的成员:数据属性、访问器属性、方法。

成员名的类型只可以为 字符串类型(包括空字符串) 或者 符号类型。

注意:此处成员名用于“成员声明”的时候,而不是“访问成员”的时候。

//类
class C {
  "a" = 1;           // 语法糖:a = 1;
  "1" = 2;           // 语法糖:1 = 2;
  "first name" = 3;  // 无语法糖
  "first-name" = 4;  // 无语法糖
}
//对象字面量
let o = {
  "a": 1,            // 语法糖:a: 1,
  "1": 2,            // 语法糖:1: 2,
  "first name": 3,   // 无语法糖
  "first-name": 4    // 无语法糖
};

实际上,比较两个成员名是否相等采用的是相等 == 运算符。先进行强制类型转换为字符串类型,再比较两个成员名是否相等,如果相等,则返回 true。比如上面的示例:

//类
"a" = 1;  // 语法糖:a = 1;
"1" = 2;  // 语法糖:1 = 2;
//对象字面量
"a": 1,   // 语法糖:a: 1,
"1": 2,   // 语法糖:1: 2,

9.可计算名

可计算名语法用于使用表达式的返回值作为成员名。

支持类的成员:静态字段、静态访问器属性、静态方法、实例字段、实例访问器属性、实例方法。

支持对象字面量的成员:数据属性、访问器属性、方法。

[表达式]

方括号 [] 内可以为任何 JavaScript 表达式,具体参考“表达式语句”章节。

表达式的返回值的类型只可以为 字符串类型(包括空字符串) 或者 符号类型。

注意:可计算名语法用于“成员声明”的时候,而不是“访问成员”的时候。

//类
let name = "a";

class C {
  [name] = 1;
}

let o = new C();
console.log(o);  // { a: 1 }
//对象字面量
let name = "a";

let o = {
  [name]: 1
};

console.log(o);  // { a: 1 }
//对象字面量解构赋值
let name = "a";

let { [name]: b } = { a: 1 };
console.log(b);  // 1

let { [name]: a } = { a: 1 };
//注意:当属性名与变量名相同时,不支持简写语法。
let { [name] } = { a: 1 };
console.log(a);  // 1

10.访问成员

[] 方式用于使用表达式的返回值作为成员名。

//类
类名[静态字段名]
类名[静态访问器属性名]
类名[静态方法名]()
对象名[实例字段名]
对象名[实例访问器属性名]
对象名[实例方法]()
new 类名()[实例字段名]
new 类名()[实例访问器属性名]
new 类名()[实例方法名]()

//对象字面量
对象名[数据属性名]
对象名[访问器属性名]
对象名[方法名]()

方括号 [] 内可以为任何 JavaScript 表达式,具体参考“表达式语句”章节。

表达式的返回值的类型只可以为 字符串类型(包括空字符串) 或者 符号类型。

//类创建对象方式
let name = "b";

class C {
  "a" = 1;
  "1" = 2;
  "first name" = 3;
  "first-name" = 4;
  [name] = 5;
}

let o = new C();
o["a"];           // 语法糖:o.a;
o["1"];           // 语法糖:o[1];
o["first name"];  // 无语法糖
o["first-name"];  // 无语法糖
o["b"];           // 语法糖:o.b;
o[name];
//对象字面量创建对象方式
let name = "b"; 

let o = {
  "a": 1,
  "1": 2,
  "first name": 3,
  "first-name": 4,
  [name]: 5
};

o["a"];           // 语法糖:o.a;
o["1"];           // 语法糖:o[1];
o["first name"];  // 无语法糖
o["first-name"];  // 无语法糖
o["b"];           // 语法糖:o.b;
o[name];
//Object()构造函数创建对象方式
let name = "b";

let o = new Object();
o["a"] = 1;           // 语法糖:o.a = 1;
o["1"] = 2;           // 语法糖:o[1] = 2;
o["first name"] = 3;  // 无语法糖
o["first-name"] = 4;  // 无语法糖
o["b"] = 5;           // 语法糖:o.b = 5;
o[name] = 6;

. 方式只支持 JavaScript 标识符。

//类
类名.静态字段名
类名.静态访问器属性名
类名.静态方法名()
对象名.实例字段名
对象名.实例访问器属性名
对象名.实例方法名()
new 类名().实例字段名
new 类名().实例访问器属性名
new 类名().实例方法名()

//对象字面量
对象名.数据属性名
对象名.访问器属性名
对象名.方法名()

11.属性简写

注意:类没有属性简写语法。

在对象字面量创建对象方式中,当属性名和代表属性值的变量名相同时,可以省略属性名以及紧跟在属性名后面的冒号,只使用变量名。

let personName = "张三";
//属性名与变量名不相同(前面name是属性名,后面personName是变量名)
let person = {
  name: personName
};
console.log(person);  // { name: "张三" }
let name = "张三";
//属性名与变量名相同(前面name是属性名,后面name是变量名)
let person = {
  name: name
};
//省略属性名以及紧跟在属性名后面的冒号,只使用变量名。
let person = {
  name
};
console.log(person);  // { name: "张三" }

当对象字面量作为函数的返回值时,也支持属性简写。

function makePerson(name) {
  return {
    name
  };
}

let person = makePerson("张三");
console.log(person);  // { name: "张三" }

12.访问器属性(Accessor Property)

12.1类

class ClassWithGetSet {
  
  //静态getter
  static get staticGetter() {
    //访问器属性体
  }

  //静态setter
  static set staticSetter(形参) {
    //访问器属性体
  }
  
  //实例getter
  get instanceGetter() {
    //访问器属性体
  }

  //实例setter
  set instanceSetter(形参) {
    //访问器属性体
  }

}

12.2对象字面量

let 对象名 = {

  //实例getter
  get instanceGetter() {
    //访问器属性体
  },

  //实例setter
  set instanceSetter(形参) {
    //访问器属性体  
  }

};

13.静态初始化块(Static Initialization Block)

静态初始化块是用于类的初始化。

一个类非必须有静态初始化块。如果一个类有多个静态初始化块,后声明的静态初始化块不会覆盖之前声明的静态初始化块,会按声明时的顺序依次执行。

class 类名 {
  static {
    //静态初始化块体
  }
}
class ClassWithStaticInitializationBlock {
  static staticProperty1 = 'Property 1';
  static staticProperty2;
  static {
    this.staticProperty2 = 'Property 2';
  }
}

console.log(ClassWithStaticInitializationBlock.staticProperty1);  // Property 1

console.log(ClassWithStaticInitializationBlock.staticProperty2);  // Property 2

14.构造函数

构造函数是用于对象的初始化。

一个类非必须有构造函数。如果一个类没有构造函数,相当于定义了一个函数体为空的构造函数。如果一个类有构造函数,则只能有一个构造函数,否则会报错 SyntaxError: property name constructor appears more than once in object literal

如果构造函数的函数体不包含 return 语句,则在创建新对象时会隐式地返回这个新对象。

如果构造函数的函数体包含 return 语句,但 return 关键字后没有表达式 或者 return 关键字后是原始类型的值,则原始类型的值会被忽略,在创建新对象时仍然会隐式地返回这个新对象。

如果构造函数的函数体包含 return 语句,但 return 关键字后为另一个新对象,则在创建新对象时会显式地返回另一个新对象。

class 类名 {
  constructor() {
    //构造函数体
  }
}

15.继承

一个子类只可以直接继承自一个父类,不可以直接继承自多个父类。

子类会直接继承父类中可继承的成员,间接继承祖父类中可继承的成员,以此类推,直至没有可继承的成员。

继承支持静态字段、静态访问器属性、静态方法、静态初始化块、实例字段、实例访问器属性、实例方法、构造函数。

注意:从父类中继承过来的成员无需在子类中重新声明,否则会发生覆盖。

//类声明
class 子类名 extends 父类名 {
  //子类体
}
//类表达式
//命名类表达式
{ let | const } C = class C2 extends 父类名 {
  //子类体
};

//匿名类表达式
{ let | const } C = class extends 父类名 {
  //子类体
};

16.覆盖(Override)

如果从父类中继承过来的成员在子类中重新声明,此时相当于在子类中同时声明了两个相同名称的成员,则在子类中重新声明的成员会覆盖从父类中继承过来的成员。

注意:只需要成员名称相同,就会发生覆盖。

注意:关于支持覆盖的成员,参考继承章节。

注意:覆盖并不会影响父类中原来的成员。

class A {
  sayName() {
    console.log('A');
  }
}

class B extends A {
  sayName() {
    console.log('B');
  }
}

new B().sayName();  // B

17.抽象类

JavaScript 语言没有专门支持抽象类的语法。

通过在抽象父类的构造函数中检查是否定义某个成员,可以要求子类必须定义某个成员。

class Vehicle {
  constructor() {
    if (!this.foo) {
      throw new Error('Inheriting class must define foo().');
    }

    console.log('success!');
  }
}

class Bus extends Vehicle {
  foo() {}
}

class Van extends Vehicle {}

new Bus();  // success!

new Van();  // Error:Inheriting class must define foo().

通过在抽象父类的构造函数中检查 new.target 的值是否 === (全等)抽象父类名,可以阻止对抽象父类的实例化。

如果 new.target 所在的函数没有被 new 调用,则 new.target 的值为 undefined

如果 new.target 所在的函数被 new 调用,则 new.target 的值为 new 所调用的类名。

class Vehicle {
  constructor() {
    console.log(new.target);  
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated.');
    }
  }
}

class Bus extends Vehicle {}

new Bus();      // class Bus {}

new Vehicle();  // class Vehicle {}
                // Error: Vehicle cannot be directly instantiated.

18.创建对象

18.1类创建方式

//如果没有实参传递,则可以省略圆括号,一般不推荐省略。
{ let | const } 对象名 = new 类名();
{ let | const } 对象名 = new 类名(实参);

18.2对象字面量创建方式

注意:赋值运算符右侧期望的是表达式,所以右侧的花括号 {} 处于表达式上下文,表示的是对象字面量表达式的开始和结束。如果花括号 {} 处于语句上下文,则表示的是块语句的开始和结束。

注意:最后一个属性(或方法)后面允许有逗号 ,,这样要添加一个新属性(或新方法)时很方便。

{ let | const } 对象名 = {
  数据属性名: 值,

  方法名: function() {
    //方法体
  },

  //方法定义简写
  方法名() {
    //方法体
  }
};

18.3Object()构造函数创建方式

Object() 构造函数创建方式目前已很少使用,而类创建方式和对象字面量创建方式非常流行。

//等同的对象字面量创建方式{ let | const } 对象名 = {};,但对象字面量创建方式实际上并不会调用Object()构造函数。
{ let | const } 对象名 = new Object();
//访问成员
对象名.属性名 = 值;
对象名.方法名 = function() {
  //方法体
};

19.super关键字

super 关键字用于在子类中调用父类的成员。

支持类的成员:静态字段、静态访问器属性、静态方法、实例访问器属性、实例方法、构造函数,不支持静态初始化块、实例字段。

支持对象字面量的成员:数据属性、访问器属性、方法。

注意:当 super 关键字用于构造函数时,父类的构造函数必须被子类的构造函数覆盖,super 关键字必须在子类的构造函数体内使用,且 super 关键字必须在 this 关键字之前使用。

//调用父类的构造函数
//无实参(注意:圆括号不可以省略)
super()
//有实参
super(实参)
//调用父类的成员
//[]方式
super[静态字段名]
super[静态访问器属性名]
super[静态方法名]()
super[实例访问器属性名]
super[实例方法名]()

//.方式
super.静态字段名
super.静态访问器属性名
super.静态方法名()
super.实例访问器属性名
super.实例方法名()
//调用父对象字面量的属性和方法
//[]方式
super[数据属性名]
super[访问器属性名]
super[方法名]()

//.方式
super.数据属性名
super.访问器属性名
super.方法名()

20.this关键字

this 关键字的指向问题非常复杂,建议遇到具体问题时具体分析。

20.1Function.prototype.bind()实例方法

bind() 实例方法用于将新函数中的 this 或 形参永久预绑定到指定实参,后期无论如何调用此新函数,新函数中的 this 或 形参的值都为指定实参。

const 新函数名 = 老函数名.bind(thisArg);
const 新函数名 = 老函数名.bind(null, arg1, arg2, argN);
const 新函数名 = 老函数名.bind(thisArg, arg1, arg2, argN);

thisArg:新函数中 this 绑定的实参。

arg:新函数中形参绑定的实参。

返回值:返回一个除了与老函数的函数名不相同的新函数。

注意:bind() 实例方法的作用是将新函数中的 this 或 形参绑定,并不是将老函数中的 this 或 形参绑定。

//老函数无形参
function f() {
  return this.a;
}

const g = f.bind({ a: 1 });

const h = f.bind({ a: 2 });

//g函数中的this已绑定,不可以再绑定。
const k = g.bind({ a: 2 });

const o = { a: 6, f, g, h, k };
console.log(o.a, o.f(), o.g(), o.h(), o.k());  // 6 6 1 2 1
//老函数有形参
function f(b, c) {
  console.log(this.a + b + c);
}

//只预绑定新函数中的this
const g = f.bind({ a: 1 });
g(2, 3);  // 6

//预绑定新函数中的this和部分形参
const g = f.bind({ a: 1 }, 2);
g(3);  // 6

//预绑定新函数中的this和所有形参
const g = f.bind({ a: 1 }, 2, 3);
g();  // 6
g(5, 6);  // 6

原创文章,作者:huoxiaoqiang,如若转载,请注明出处:https://www.huoxiaoqiang.com/javascript/javascriptlang/4729.html

(0)
huoxiaoqiang的头像huoxiaoqiang
上一篇 2020年9月3日 02:23
下一篇 2020年9月5日 23:17

相关推荐

  • 2.11JavaScript语言的异步函数

    1.异步函数声明 如果您想声明一个异步函数,只需要首先在普通函数前添加一个 async 关键字,然后在函数体内至少包含一个 await 关键字即可。普通函数支持函数声明、函数表达式、箭头函数、立即调用的函数表达式和实例方法。 2.await表达式 await 关键字后的表达式可以是 实现了 Thenable 接口的对象…

  • 2.1JavaScript语言的ES模块(Module)

    ES 模块用于在浏览器环境和服务器环境中使用。 模块就是以 .js 为扩展名的 JavaScript 文件。 普通脚本文件内的顶级的成员对其它脚本文件来说是公开(public)的全局上下文,而模块文件内的顶级的模块成员对其它模块文件来说都是私有(private)的,所以首先需要在模块中将它们显式导出,然后在其它模块中显…

  • 2.9JavaScript引用类型之生成器(Generator)

    Generator 类型实现了 Iterable 接口。 1.生成器函数声明 生成器函数声明只需要在函数名前面加一个星号 * 即可,星号 * 不受两侧空格的影响。 生成器函数支持函数声明、函数表达式,不支持箭头函数表达式。 2.yield和yield * yield 关键字用来多次暂停和恢复一个生成器函数,它…

发表回复

登录后才能评论