Class:向传统类模式转变的构造函数
前言
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
不同,更为类似let
和const
,不会提前解析,不存在变量提升,不与全局作用域挂钩和拥有暂时性死区等。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
,再通过call
或apply
方法,在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
,再通过父类构造函数进行修饰,因此无法获取到父类的内部属性(隐藏属性)。另一方面,原生构造函数会直接忽略call
或apply
方法传入的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执行权的真实操作者
上一篇: 并发编程-多线程
下一篇: Http协议与TCP协议有什么区别