Class
JavaScript 语言中,生成实例对象的传统方法是通过构造函数
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
上面的代码用 ES6 的class改写,就是下面这样
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES5 的构造函数 Point
,对应 ES6 的 Point
类的构造方法
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
类属性除了通过 constructor
内部定义,也可以写在类的最顶层
class Point {
constructor() {
this.x = 1;
this.y = 2;
}
}
// 等同于
class Point {
_x = 1;
_y = 2
}
constructor 方法
constructor
方法是类的默认方法,通过 new
命令生成对象实例时,自动调用该方法。一个类必须有 constructor
方法,如果没有显式定义,一个空的 constructor
方法会被默认添加
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor
方法默认返回实例对象(即 this
),完全可以指定返回另外一个对象
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
上面代码中,constructor
函数返回一个全新的对象,结果导致实例对象不是 Foo
类的实例
类必须使用 new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new
也可以执行
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
类方法
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
上面给 Point
类定义了 toString
和 toValue
方法,事实上,类的所有方法都定义在类的 prototype
属性上面
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
因此在类的实例上面调用方法,其实就是调用原型上的方法
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
类的内部所有定义的方法,都是不可枚举的(non-enumerable),这一点与 ES5 的行为不一致。
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
class ClassPoint {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
Object.keys(ClassPoint.prototype)
// []
Object.getOwnPropertyNames(ClassPoint.prototype)
// ["constructor","toString", "toValue"]
静态属性和静态方法
静态属性和静态方法的特点:
只能通过类访问
可以被子类继承
静态属性
静态属性指的是 Class 本身的属性,即 Class.propName
,而不是定义在实例对象( this
)上的属性
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
// 继续
class Fo extends Foo{
}
Fo.prop // 1
要注意的静态方法可以写在 Class 内部, 而静态属性写在 Class 外部。因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上 static
关键字
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
静态方法
在类方法前面加上 static
关键字,就变成了“静态方法”
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod() // 静态属性和静态方法都通过实例访问
// TypeError: foo.classMethod is not a function
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
TIP
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例
私有方法和私有属性
ES6 没有提供私有方法和属性,只能通过变通方法模拟实现
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
上面代码中,foo
是公开方法,内部调用了 bar.call(this, baz)
。这使得 bar
实际上成为了当前模块的私有方法
还有一种方法是利用 Symbol 值的唯一性,将私有方法的名字命名为一个 Symbol 值
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
上面代码中,bar
和 snaf
都是 Symbol 值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行, Reflect.ownKeys()
依然可以拿到它们
const inst = new myClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
上面代码中,Symbol 值的属性名依然可以从类的外部拿到
类的实例
实例的属性显式定义在其本身(即定义在 this
对象上),类方法定义在原型上
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
与 ES5 一样,类的所有实例共享一个原型对象
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
这也意味着,可以通过实例的 __proto__
属性为“类”添加方法
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
getter/setter
与 ES5 一样,在“类”的内部可以使用 get
和 set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
存值函数和取值函数是设置在属性的 Descriptor
对象上的
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);
"get" in descriptor // true
"set" in descriptor // true
上面代码中,Square
类的方法名 getArea
,是从表达式得到的
Class 表达式
与函数一样,类也可以使用表达式的形式定义
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是 Me
,但是 Me
只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用 MyClass
引用
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码表示,Me
只在 Class 内部有定义。
如果类的内部没用到的话,可以省略 Me
,也就是可以写成下面的形式。
const MyClass = class { /* ... */ };
采用 Class 表达式,可以写出立即执行的 Class
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
上面代码中,person
是一个立即执行的类的实例
new.target
ES6 为 new
命令引入了一个 new.target
属性,该属性一般用在构造函数之中,返回 new
命令作用于的那个构造函数
如果构造函数不是通过 new
命令或 Reflect.construct()
调用的,new.target
会返回 undefined
,因此这个属性可以用来确定构造函数是怎么调用的
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
上面代码确保构造函数只能通过 new
命令调用。
Class 内部调用 new.target
,返回当前 Class
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // 输出 true
需要注意的是,子类继承父类时,new.target
会返回子类,利用这个特点,可以写出不能独立使用、必须继承后才能使用的类
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
总结
静态属性和方法只能通过类访问且能被子类继承,但不能通过实例访问
目前不能直接实现
私有属性和私有方法
创建实例后,类的属性定义在实例本身,类的方法定义在实例的原型对象上
与普通方法的区别
类的内部所有定义的方法,都是不可枚举的
class类必须new调用,不能直接执行
class类不存在变量提升
class类有static静态方法
Babel 对类的转换
类的实现
粟子:
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
static showName(){
console.log('showName')
}
}
Parent.some = 'some'
经 babel 转码之后
"use strict";
function _classCallCheck(instance, Constructor) {
// 判断是不是通过 new 来调用这个方法
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false; // ES6 类方式和静态方法是不可枚举的,所以设置 enumerable 为 false
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor); // 通过 `Object.defineProperty` 代理到新的实例对象中
}
}
function _createClass(Constructor, protoProps, staticProps) {
// 通过参数名可以推测调用 defineProperties 方法时,对于类方法
// 将类方法赋值到 Constructor.prototype 上
// 对于类的静态方法调用 defineProperties 方法,将静态方法赋值到 Constructor 属性上
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
// 修改了 Parent 的定义,通过自执行函数返回内容的 Parent 函数
let Parent = /*#__PURE__*/ (function () {
function Parent(name, age) {
_classCallCheck(this, Parent); // 判断是不是通过 new 调用函数
this.name = name;
this.age = age;
}
_createClass( // 给当前类添加类方法和静态方法
Parent,
[
{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}
],
[
{
key: "showName",
value: function showName() {
console.log("showName");
}
}
]
);
return Parent;
})();
Parent.some = "some"; // 直接赋值静态属性
从上面的例子可以看到并结合上文对类的解析,可以知道 Babel 对类的转换大体为以下步骤:
通过
instance instanceof Constructor
,判断是否是通过new
方法调用父类类属性属性赋值到实例对象上
类方法设置在对象的原型中
静态方法和静态属性直接设置在实例对象中
类方法和静态方法设置为不可枚举属性
继承实现
粟子
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
speakSomething(){
console.log("I can speek chinese");
}
static showName(){
console.log('showName')
}
}
Parent.some = 'some'
class Child extends Parent {
constructor(name,age){
super(name,age);
}
coding(){
console.log("I can code JS");
}
}
var c = new Child("job",30);
c.coding()
转码之后的代码变成了这样
"use strict";
function _inherits(subClass, superClass) {
// 父类只能是函数
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 继承父类的类方法,添加 constructor 属性 值为自身
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
});
// 设置 __proto__
// 子类也是一函数,它的 __proto__ 指类父类
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct(); // 是否支持 Reflect
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived), // 获取父类
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget); //Reflect.construct() 方法的行为有点像 new 操作符构造函数 , 相当于运行 new target(...args).
} else {
result = Super.apply(this, arguments); // 上面分支不好理解的各话,就理解这里,调用父类,设置给当前的子类实例添加类属性
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
// 实现父类
let Parent = /*#__PURE__*/ (function () {
function Parent(name, age) {
_classCallCheck(this, Parent);
this.name = name;
this.age = age;
}
_createClass(
Parent,
[
{
key: "speakSomething",
value: function speakSomething() {
console.log("I can speek chinese");
}
}
],
[
{
key: "showName",
value: function showName() {
console.log("showName");
}
}
]
);
return Parent;
})();
Parent.some = "some";
let Child = /*#__PURE__*/ (function (_Parent) {
_inherits(Child, _Parent); // 子类继续父类方法,并设置 __proto__
var _super = _createSuper(Child); // 返回 _createSuperInternal 方法
function Child(name, age) { // 创建子类方法
_classCallCheck(this, Child);
return _super.call(this, name, age);
}
// 给子类添加方法
_createClass(Child, [
{
key: "coding",
value: function coding() {
console.log("I can code JS");
}
}
]);
return Child;
})(Parent);
var c = new Child("job", 30);
c.coding();
总结一下流程:
用创建类的步骤创建子类 Child
调用
_inherits(Child, _Parent)
方法,继承父类的类方法同时添加 constructor 属性指向为自身(Fn.prototype.constructor = Fn)
这两步完成的子类的创建
当子类被 new
时执行 _super.call(this, name, age)
,相当于执行父类方法,通过 call
将 this
指向当前实例,给实例添加父类属性