典型的面向对象编程语言(比如C++和Java)存在类(class)这个概念。所谓类就是对象的模板,对象是类的实例
JS中没有类,在ES5中使用构造函数(constructor)作为对象的模板。但是ES5中实现继承的代码非常冗长与混乱(盗用构造函数、组合继承、原型式继承等等),因此在ES6中新引入了class关键字具有了正式定义类的能力
类(class)是ECMAScript中新的基础性语法糖结构。其背后使用的仍然是原型和构造函数的概念
1.类定义
class Person {}
const Person = class {}
说明:
- class声明不存在变量提升:
-
console.log(classDemo); class classDemo {}; // var_let_const.html:12 Uncaught ReferenceError: Cannot access 'classDemo' before initialization
-
- 函数受函数作用域限制,而类受块作用域限制:
-
{ class classDemo { }; } console.log(classDemo); // var_let_const.html:16 Uncaught ReferenceError: classDemo is not defined
-
- 类名首字母同函数构造函数一样,建议类名首字母大写
2.类构造函数
constructor关键字用于在类定义块内部创建类的构造函数。方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。
构造函数非必须,不定义相当于构造函数为空
示例:
class Person { constructor(override){ console.log("我是一个人") } }; let p1 = new Person() // 我是一个人
(1)使用new实例化类
使用new实例化的操作等于使用new调用其构造函数,new执行的操作有:
- 在内存中创建一个新对象
- 这个新对象内部的__proto__属性赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(this指向了新对象)
- 执行构造函数内部的代码
- 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象
说明:
- 类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号是可选的
- 类构造函数与构造函数的主要区别在于:
- 调用类构造函数必须使用new操作符。不使用new则会抛出错误(Uncaught TypeError: Class constructor Person cannot be invoked without 'new')
- 而普通构造函数如果不使用new调用,那么就会以全局的this(window)作为内部对象
(2)把类当成特殊函数
JS中并没有正式的类这个类型。从各方面看,ECMAScript的类就是一种特殊的函数。可以使用typeof 检测表明它是一个函数:
class Person { }; console.log(typeof Person); // function
- 可以使用instanceof操作符检查一个对象是不是类的实例:
1 class Person {}; 2 3 let p = new Person() 4 5 console.log(p instanceof Person); // true
- 类也可以向其他对象或者函数引用一样把类作为参数传递
1 let pList = [ 2 class Person { 3 constructor ( id ){ 4 this._id = id 5 console.log("类作为参数:",this._id) 6 } 7 } 8 ] 9 10 function createIns( classDefinition, id ){ 11 return new classDefinition(id) 12 } 13 14 let foo = createIns(pList[0],1234) // 类作为参数: 1234
- 类还可以立即实例化
1 let foo = new class Person { 2 constructor(id) { 3 console.log("立即实例化对象") 4 } 5 }
3.实例、原型和类成员
每次通过new调用类标识符时,都会执行类构造函数,在这个函数内部,可以为新创建的实例(this)添加自有属性。且构造函数执行完毕以后,仍然可以给实例继续添加新成员。
(1)实例成员
每个实例都对应一个唯一的成员对象,即所有成员都不会在原型上共享:
1 class Person { 2 constructor(id) { 3 this.name = new String('Jack') 4 5 this.sayName = () => console.log(this.name); 6 7 this.nickNames = ['张三','李四'] 8 } 9 } 10 11 let p1 = new Person(),p2 = new Person(); 12 13 p1.sayName(); // Jack 14 p2.sayName(); // Jack 15 console.log(p1.name === p2.name); // false 16 console.log(p1.sayName === p2.sayName); // false 17 console.log(p1.nickNames === p2.nickNames); // false
(2)原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法
1 class Person { 2 constructor() { 3 // 存在这个类的不同的实例上 4 this.locate = () => console.log("instance"); 5 } 6 // 在类块中定义的所有内容都会定义在类的原型上 7 locate() { 8 console.log("prototype"); 9 } 10 } 11 12 let p = new Person() 13 14 p.locate() // instance 15 Person.prototype.locate() // prototype
- 可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据
1 class Person { 2 name: '张三' 3 } 4 // Uncaught SyntaxError: Unexpected identifier
- 类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键
1 let funName = "fn02" 2 3 class Person { 4 5 fn01(){ 6 console.log("字符串属性名"); 7 } 8 9 [funName](){ 10 console.log("变量属性名"); 11 } 12 13 ['fn' + '03'](){ 14 console.log("计算属性名"); 15 } 16 17 } 18 19 let p = new Person() 20 p.fn01() // 字符串属性名 21 p.fn02() // 变量属性名 22 p.fn03() // 计算属性名
- 类定义也支持get与set访问器:
1 class Person { 2 3 set name(newName){ 4 console.log("设置新值为:",newName); 5 this._name = newName 6 } 7 8 get name(){ 9 console.log("读取到新值:",this._name); 10 return this._name 11 } 12 13 } 14 15 let p = new Person() 16 p.name = "张三" // 设置新值为: 张三 17 p.name // 读取到新值: 张三
(3)非函数原型和类成员
类定义并不显式支持在原型或类上添加成员数据,但是可以在类定义外部手动添加:
1 class Person { 2 3 sayName() { 4 console.log(`${ Person.greeting }${ this.name }`); 5 } 6 7 } 8 // 在类上定义数据成员 9 Person.greeting = ' 我的名字是' 10 // 在原型上定义数据成员 11 Person.prototype.name = '张三' 12 13 let p = new Person() 14 p.sayName() // 我的名字是张三