前言

JS基于原型的‘类’,一直被转行前端的码僚们大呼惊奇,但接近传统模式使用class关键字定义的出现,却使得一些前端同行深感遗憾而纷纷留言:“还我独特的JS”、“净搞些没实质的东西”、“自己没有类还非要往别家的类上靠”,甚至是“已转行”等等。有情绪很正常,毕竟新知识意味着更多时间与精力的开销,又不是简单的闭眼享受。

然而历史的轴印前行依旧,对于class可以肯定的一点是你不能对面试官说:“拜托,不是小弟不懂,仅仅是不愿意了解,您换个问题呗!”一方面虽然class只是个语法糖,但extends对继承的改进还是不错的。另一方面今后可能在‘类’上出现的新特性应该是由class而不是构造函数承载,谁也不确定它将来会出落得怎样标致。因此,来来来,慢慢的喝下这碗热气腾腾的红糖姜汤。

1 class

ECMAScript中没有类的概念,我们的实例是基于原型由构造函数生成具有动态属性和方法的对象。不过为了与国际接轨,描述的更为简便和高大上,依然会使用‘类’这一词。所以JS的类等同于构造函数。ES6的class只是个语法糖,其定义生成的对象依然构造函数。不过为了与构造函数模式区分开,我们称其为类模式。学习class需要有构造函数和原型对象的知识,具体可以自行百度。


// ---使用构造函数
function C () {
console.log('New someone.');
} C.a = function () { return 'a'; }; // 静态方法 C.prototype.b = function () { return 'b'; }; // 原型方法 // ---使用class
class C {
static a() { return 'a'; } // 静态方法 constructor() { console.log('New someone.'); } // 构造方法 b() { return 'b'; } // 原型方法
};

1.1 与变量对比

关键字class类似定义函数的关键字function,其定义的方式有声明式和表达式(匿名式和命名式)两种。通过声明式定义的变量的性质与function不同,更为类似letconst,不会提前解析,不存在变量提升,不与全局作用域挂钩和拥有暂时性死区等。class定义生成的变量就是一个构造函数,也因此,类可以写成立即执行的模式。



// ---声明式
class C {}
function F() {} // ---匿名表达式
let C = class {};
let F = function () {}; // ---命名表达式
let C = class CC {};
let F = function FF() {}; // ---本质是个函数
class C {}
console.log(typeof C); // function
console.log(Object.prototype.toString.call(C)); // [object Function]
console.log(C.hasOwnProperty('prototype')); // true // ---不存在变量提升
C; // 报错,不存在C。
class C {}
// 存在提前解析和变量提升
F; // 不报错,F已被声明和赋值。
function F() {} // ---自执行模式
let c = new (class {
})();
let f = new (function () {
})();

1.2 与对象对比

类内容({}里面)的形式与对象字面量相似。不过类内容里面只能定义方法不能定义属性,方法的形式只能是函数简写式,方法间不用也不能用逗号分隔。方法名可以是带括号的表达式,也可以为Symbol值。方法分为三类,构造方法(constructor方法)、原型方法(存在于构造函数的prototype属性上)和静态方法(存在于构造函数本身上)



class C {
// 原型方法a
a() { console.log('a'); }
// 构造方法,每次生成实例时都会被调用并返回新实例。
constructor() {}
// 静态方法b,带static关键字。
static b() { console.log('b'); }
// 原型方法,带括号的表达式
['a' + 'b']() { console.log('ab'); }
// 原型方法,使用Symbol值
[Symbol.for('s')]() { console.log('symbol s'); }
} C.b(); // b let c = new C();
c.a(); // a
c.ab(); // ab
c[Symbol.for('s')](); // symbol s

不能直接定义属性,并不表示类不能有原型或静态属性。解析class会形成一个构造函数,因此只需像为构造函数添加属性一样为类添加即可。更为直接也是推荐的是只使用getter函数定义只读属性。为什么不能直接设置属性?是技术不成熟?是官方希望传递某种思想?抑或仅仅是笔者随意抛出的一个问题?


// ---直接在C类(构造函数)上修改
class C {}
C.a = 'a';
C.b = function () { return 'b'; };
C.prototype.c = 'c';
C.prototype.d = function () { return 'd'; }; let c = new C();
c.c; // c
c.d(); // d // ---使用setter和getter
// 定义只能获取不能修改的原型或静态属性
class C {
get a() { return 'a'; }
static get b() { return 'b'; }
} let c = new C();
c.a; // a
c.a = '1'; // 赋值没用,只有get没有set无法修改。

1.3 与构造函数对比

下面是使用构造函数和类实现相同功能的代码。直观上,class简化了代码,使得内容更为聚合。constructor方法体等同构造函数的函数体,如果没有显式定义此方法,一个空的constructor方法会被默认添加用于返回新的实例。与ES5一样,也可以自定义返回另一个对象而不是新实例。


// ---构造函数
function C(a) {
this.a = a;
} // 静态属性和方法
C.b = 'b';
C.c = function () { return 'c'; }; // 原型属性和方法
C.prototype.d = 'd';
C.prototype.e = function () { return 'e'; };
Object.defineProperty(C.prototype, 'f', { // 只读属性
get() {
return 'f';
}
}); // ---类
class C {
static c() { return 'c'; } constructor(a) {
this.a = a;
} e() { return 'e'; }
get f() { return 'f'; }
} C.b = 'b';
C.prototype.d = 'd';

类虽然是个函数,但只能通过new生成实例而不能直接调用。类内部所定义的全部方法是不可枚举的,在构造函数本身和prototype上添加的属性和方法是可枚举的。类内部定义的方法默认是严格模式,无需显式声明。以上三点增加了类的严谨性,比较遗憾的是,依然还没有直接定义私有属性和方法的方式。


// ---能否直接调用
class C {}
C(); // 报错 function C() {}
C(); // 可以 // ---是否可枚举
class C {
static a() {} // 不可枚举
b() {} // 不可枚举
} C.c = function () {}; // 可枚举
C.prototype.d = function () {}; // 可枚举 isEnumerable(C, ['a', 'c']); // a false, c true
isEnumerable(C.prototype, ['b', 'd']); // b false, d true function isEnumerable(target, keys) {
let obj = Object.getOwnPropertyDescriptors(target);
keys.forEach(k => {
console.log(k, obj[k].enumerable);
});
} // ---是否为严格模式
class C {
a() {
let is = false;
try {
n = 1;
} catch (e) {
is = true;
}
console.log(is ? 'true' : 'false');
}
} C.prototype.b = function () {
let is = false;
try {
n = 1;
} catch (e) {
is = true;
}
console.log(is ? 'true' : 'false');
}; let c = new C();
c.a(); // true,是严格模式。
c.b(); // false,不是严格模式。

在方法前加上static关键字表示此方法为静态方法,它存在于类本身,不能被实例直接访问。静态方法中的this指向类本身。因为处于不同对象上,静态方法和原型方法可以重名。ES6新增了一个命令new.target,指代new后面的构造函数或class,该命令的使用有某些限制,具体请看下面示例。


// ---static
class C {
static a() { console.log(this === C); }
a() { console.log(this instanceof C); }
} let c = new C();
C.a(); // true
c.a(); // true // ---new.target
// 构造函数
function C() {
console.log(new.target);
} C.prototype.a = function () { console.log(new.target); }; let c = new C(); // 打印出C
c.a(); // 在普通方法中为undefined。 // ---类
class C {
constructor() { console.log(new.target); }
a() { console.log(new.target); }
} let c = new C(); // 打印出C
c.a(); // 在普通方法中为undefined。 // ---在函数外部使用会报错
new.target; // 报错

2 extends

ES5中的经典继承方法是寄生组合式继承,子类会分别继承父类实例和原型上的属性和方法。ES6中的继承本质也是如此,不过实现方式有所改变,具体如下面的代码。可以看到,原型上的继承是使用extends关键字这一更接近传统语言的形式,实例上的继承是通过调用super完成子类this塑造。表面上看,方式更为的统一和简洁。


class C1 {
constructor(a) { this.a = a; }
b() { console.log('b'); }
} class C extends C1 { // 继承原型数据
constructor() {
super('a'); // 继承实例数据
}
}

2.1 与构造函数对比

使用extends继承,不仅仅会将子类的prototype属性的原型对象(__proto__)设置为父类的prototype,还会将子类本身的原型对象(__proto__)设置为父类本身。这意味着子类不单单会继承父类的原型数据,也会继承父类本身拥有的静态属性和方法。而ES5的经典继承只会继承父类的原型数据。不单单是财富,连老爸的名气也要获得,不错不错。


class C1 {
static get a() { console.log('a'); }
static b() { console.log('b'); }
} class C extends C1 {
}
// 等价,没有构造方法会默认添加。
class C extends C1 {
constructor(...args) {
super(...args);
}
} let c = new C();
C.a; // a,继承了父类的静态属性。
C.b(); // b,继承了父类的静态方法。
console.log(Object.getPrototypeOf(C) === C1); // true,C的原型对象为C1
console.log(Object.getPrototypeOf(C.prototype) === C1.prototype); // true,C的prototype属性的原型对象为C1的prototype

ES5中的实例继承,是先创造子类的实例对象this,再通过callapply方法,在this上添加父类的实例属性和方法。当然也可以选择不继承父类的实例数据。而ES6不同,它的设计使得实例继承更为优秀和严谨。

在ES6的实例继承中,是先调用super方法创建父类的this(依旧指向子类)和添加父类的实例数据,再通过子类的构造函数修饰this,与ES5正好相反。ES6规定在子类的constructor方法里,在使用到this之前,必须先调用super方法得到子类的this。不调用super方法,意味着子类得不到this对象。


class C1 {
constructor() {
console.log('C1', this instanceof C);
}
} class C extends C1 {
constructor() {
super(); // 在super()之前不能使用this,否则报错。
console.log('C');
}
} new C(); // 先打印出C1 true,再打印C。

2.2 super

关键字super比较奇葩,在不同的环境和使用方式下,它会指代不同的东西(总的说可以指代对象或方法两种)。而且在不显式的指明是作为对象或方法使用时,比如console.log(super),会直接报错。

作为函数时。super只能存在于子类的构造方法中,这时它指代父类构造函数。

作为对象时。super在静态方法中指代父类本身,在构造方法和原型方法中指代父类的prototype属性。不过通过super调用父类方法时,方法的this依旧指向子类。即是说,通过super调用父类的静态方法时,该方法的this指向子类本身;调用父类的原型方法时,该方法的this指向该(子类的)实例。而且通过super对某属性赋值时,在子类的原型方法里指代该实例,在子类的静态方法里指代子类本身,毕竟直接在子类中通过super修改父类是很危险的。

很迷糊对吧,疯疯癫癫的,还是结合着代码看吧!


class C1 {
static a() {
console.log(this === C);
}
b() {
console.log(this instanceof C);
}
} class C extends C1 {
static c() {
console.log(super.a); // 此时super指向C1,打印出function a。 this.x = 2; // this等于C。
super.x = 3; // 此时super等于this,即C。
console.log(super.x); // 此时super指向C1,打印出undefined。
console.log(this.x); // 值已改为3。 super.a(); // 打印出true,a方法的this指向C。
} constructor() {
super(); // 指代父类的构造函数 console.log(super.c); // 此时super指向C1.prototype,打印出function c。 this.x = 2; // this等于新实例。
super.x = 3; // 此时super等于this,即实例本身。
console.log(super.x); // 此时super指向C1.prototype,打印出undefined。
console.log(this.x); // 值已改为3。 super.b(); // 打印出true,b方法的this指向实例本身。
}
}

2.3 继承原生构造函数

使用构造函数模式,构建继承了原生数据结构(比如Array)的子类,有许多缺陷的。一方面由上文可知,原始继承是先创建子类this,再通过父类构造函数进行修饰,因此无法获取到父类的内部属性(隐藏属性)。另一方面,原生构造函数会直接忽略callapply方法传入的this,导致子类根本无法获取到父类的实例属性和方法。


function MyArray(...args) {
Array.apply(this, args);
} MyArray.prototype = Array.prototype;
// MyArray.prototype.constructor = MyArray; let arr = new MyArray(1, 2, 3); // arr为对象,没有储存值。
arr.push(4, 5); // 在arr上新增了0,1和length属性。
arr.map(d => d); // 返回数组[4, 5]
arr.length = 1; // arr并没有更新,依旧有0,1属性,且arr[1]为5。

创建类的过程,是先构造一个属于父类却指向子类的this(绕口),再通过父类和子类的构造函数进行修饰。因此可以规避构造函数的问题,获取到父类的实例属性和方法,包括内部属性。进而真正的创建原生数据结构的子类,从而简单的扩展原生数据类型。另外还可以通过设置Symbol.species属性,使得衍生对象为原生类而不是自定义子类的实例。


class MyArray extends Array { // 实现是如此的简单
static get [Symbol.species]() { return Array; }
} let arr = new MyArray(1, 2, 3); // arr为数组,储存有1,2,3。
arr.map(d => d); // 返回数组[1, 2, 3]
arr.length = 1; // arr正常更新,已包含必要的内部属性。

需要注意的是继承Object的子类。ES6改变了Object构造函数的行为,一旦发现其不是通过new Object()这种形式调用的,构造函数会忽略传入的参数。由此导致Object子类无法正常初始化,但这不是个大问题。


class MyObject extends Object {
static get [Symbol.species]() { return Object; }
} let o = new MyObject({ id: 1 });
console.log(o.hasOwnPropoty('id')); // false,没有被正确初始化

推荐

ES6精华:Symbol
ES6精华:Promise
Async:简洁优雅的异步之道
Generator:JS执行权的真实操作者

Class:向传统类模式转变的构造函数的更多相关文章

  1. javascript类式继承模式#2——借用构造函数

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  2. 货运APP雨后春笋 传统物流模式将被改变

    移动互联网正在改变我们的生活方式,各种手机APP充斥着人们的生活,物流行业也不例外.货运APP的出现,对于物流行业是一个提升的机会,也是迈向标准化和专业化的一个有效途径.有专家预测,这将为物流行业的发 ...

  3. Singleton单例模式是最简单的设计模式,它的主要作用是保证在程序执行生命周期中,使用了单类模式的类仅仅能有一个实例对象存在。

                                                                                                        ...

  4. JS创建对象的四种简单方式 (工厂模式和自定义构造函数创建对象的区别)

    // 对象:特指的某个事物,具有属性和方法(一组无序的属性的集合) // 特征------>属性 // 行为------>方法 // 创建对象的四种方式 1 // 1.字面量的方式,就是实 ...

  5. 设计模式之行为类模式大PK

                                        行为类模式大PK 行为类模式包括责任链模式.命令模式.解释器模式.迭代器模式.中介者模式.备忘录模式.观察者模式.状态模式.策略 ...

  6. 设计模式之结构类模式大PK

                                      结构类模式大PK 结构类模式包括适配器模式.桥梁模式.组合模式.装饰模式.门面模式.享元模式和代理模式.之所以称其为结构类模式,是因 ...

  7. 设计模式之创建类模式大PK

                                        创建类模式大PK 创建类模式包括工厂方法模式.建造者模式.抽象工厂模式.单例模式和原型模式,他们能够提供对象的创建和管理职责.其 ...

  8. C++类继承中的构造函数和析构函数 调用顺序

    思想: 在C++的类继承中,构造函数不能被继承(C11中可以被继承,但仅仅是写起来方便,不是真正的继承) 建立对象时,首先调用基类的构造函数,然后在调用下一个派生类的构造函数,依次类推: 析构对象时, ...

  9. JavaScript之面向对象学习七(动态原型模式、寄生构造函数模式、稳妥构造函数模式创建自定义类型)

    一.动态原型模式 在面向对象学习六中的随笔中,了解到组合构造函数模式和原型模式创建的自定义类型可能最完善的!但是人无完人,代码亦是如此! 有其他oo语言经验的开发人员在看到独立的构造函数和原型时,很可 ...

随机推荐

  1. 03.Spring IoC 容器 - 初始化

    基本概念 Spring IoC 容器的初始化过程在监听器 ContextLoaderListener 类中定义. 具体由该类的的 configureAndRefreshWebApplicationCo ...

  2. adb 调系统时间

    1.修改前提 获取系统root权限,然后adb shell进入shell界面 adb shell su 2.时区设置 cat /data/property/persist.sys.timezone / ...

  3. SpringBoot | 第二十五章:日志管理之自定义Appender

    前言 前面两章节我们介绍了一些日志框架的常见配置及使用实践.一般上,在开发过程中,像log4j2.logback日志框架都提供了很多Appender,基本上可以满足大部分的业务需求了.但在一些特殊需求 ...

  4. java 核心技术 读后总结

    总结 1.少用八进制,以及二进制. 那么就是直接用16进制或10进制吗?额,想当年有这样搞过,后面就uuid了. 2.>>>用0填充高位>>用符号位填充高位<< ...

  5. electron 集成 nedb / sqlite3

    nedb nedb 无法创建文件 // webpack 构建的前提 externals: process.env.web ? {} : { "nedb": "requir ...

  6. .bak文件数据还原

    .bak文件还原(见下图) 1.连接上数据库,右键数据库,选择新建数据库,输入你要还原数据库的名称 2.数据库右键-->任务-->还原-->数据库,弹出窗口选择[源设备],选择.ba ...

  7. KendoUI 自定义验证:

    Html: <label>@LogicNameAttribute.GetLogicName(typeof(Reward).GetProperty("ExtraRewardMone ...

  8. 一起来学Spring Cloud | 第一章 :如何搭建一个多模块的springcloud项目

    在spring cloud系列章节中,本来已经写了几个章节了,但是自己看起来有些东西写得比较杂,所以重构了一下springcloud的章节内容,新写了本章节,先教大家在工作中如何搭建一个多模块的spr ...

  9. SpringBoot的特性

    SpringBoot的理念“习惯优于配置” 习惯优于配置(项目中存在大量的配置,此外还内置了一个习惯性的配置,无须手动进行配置) 使用SpringBoot可以方便地创建独立运行.准生产级别的基于Spr ...

  10. 【转】Nginx跳转任意Http请求到Https

    网站买了证书,绿条,多霸气! 那么自然得拦截http的访问方式了. 拦截http,301到https 各种Google,最后在Nginx官网找到例子,配置很简单,如下: server { listen ...