ES6入门六:class的基本语法、继承、私有与静态属性、修饰器
- 基本语法
- 继承
- 私有属性与方法、静态属性与方法
- 修饰器(Decorator)
一、基本语法
1 class Grammar{ 2 constructor(name,age){ //定义对象自身的方法和属性 3 this.name = name, 4 this.age = age 5 } 6 // 在原型上定义只读属性 7 get inva(){ 8 return "JS"; 9 } 10 //在原型上定义可读写属性 11 set skill(val){ 12 this._skill = val; 13 } 14 get skill(){ 15 return this._skill; 16 } 17 action(){ //定义原型上的方法 18 console.log("使用" + this.inva + "实现web项目。"); 19 } 20 }
将ES6的class类示例用ES5语法实现:
1 function Obj(name,age){ 2 this.name = name; 3 this.age = age; 4 } 5 Object.defineProperty(Obj.prototype,"inva",{ 6 get:function(){ 7 return "JS"; 8 }, 9 configurable:true, 10 enumerable:false 11 }); 12 Object.defineProperty(Obj.prototype,"skill",{ 13 set:function(val){ 14 this._skill = val; 15 }, 16 get:function(){ 17 return this._skill; 18 }, 19 configurable:true, 20 enumerable:false 21 }); 22 Object.defineProperty(Obj.prototype,"saction",{ 23 value:function action(){ 24 console.log("使用" + this.inva + "实现web项目。"); 25 }, 26 writable:true, 27 configurable:true, 28 enumerable:false 29 });
1.1Class简单说明:
Class声明的类本质上还是一个函数:
typeof Grammar; //"function" Grammar === Grammar.prototype.constructor; //true
类虽然是函数,但是不能直接被调用执行,必须使用new指令执行构造行为:
Grammar();//Class constructor Grammar cannot be invoked without 'new'
constructor方法是类的默认方法,一个类必须有constructor方法,如果没有显式的定义,会隐式的添加一个空的constructor方法,不会报错:
1 class ObjFun{ 2 get a(){ 3 return "这是一个没有显式定义constructor方法的类"; 4 } 5 } 6 var objFun = new ObjFun(); 7 console.log(objFun.a);//这是一个没有显式定义constructor方法的类
Class声明的类不存在变量提升,所以在类前面调用会报错:
1 new Foo(); //Cannot access 'Foo' before initialization 2 class Foo{}
二、继承
1 class InheritGrammar extends Grammar{ 2 constructor(name,age,color){ 3 super(name,age); //调用父类的constructor 4 this.color = color; 5 } 6 introduce(){ 7 this.action(); 8 console.log("我的颜色是"+this.color); 9 } 10 } 11 var inGrammar = new InheritGrammar("他乡踏雪",18,"blur"); 12 inGrammar.introduce();//使用JS实现web项目 我的颜色是blur
在控制台展开inGrammar实例:
1 InheritGrammar {name: "他乡踏雪", age: 18, color: "blur"}
将ES6的class继承语法使用ES5的语法实现:
1 function InheritObj(name,age,color){ 2 var InSuper = new Obj(name,age); //这行代码可以替换(没有差异):var InSuper = new Grammar(name,age); 3 for(var key in InSuper){ 4 this[key] = InSuper[key]; 5 } 6 this.color = color; 7 } 8 InheritObj.prototype.introduce = function introduce(){ 9 this.action(); 10 console.log("我的颜色是"+this.color); 11 } 12 var InObjPro = Object.getOwnPropertyNames(Obj.prototype); //将Obj.prototype替换成Grammar没有区别 13 for(var time of InObjPro){ 14 if(time !== "constructor"){ 15 var attrObj = Object.getOwnPropertyDescriptor(Obj.prototype,time);//将Obj.prototype替换成Grammar没有区别 16 Object.defineProperty(InheritObj.prototype,time,attrObj); 17 } 18 }
测试代码:
1 var grammar = new Grammar("他乡踏雪",18); 2 var obj = new Obj("他乡踏雪",18); 3 4 var inGrammar = new InheritGrammar("他乡踏雪",18,"blur"); 5 var inObj = new InheritObj("他乡踏雪",18,"blur"); 6 7 console.log(grammar); 8 console.log(obj); 9 console.log("............................."); 10 console.log(inGrammar); 11 console.log(inObj);
ES6的class实现可定有浏览器内部更优的实现方案,使用ES5的语法实现只是模仿,但ES6语法并不改变JS的语言特性,所以使用ES5模仿实现的class机制不会有差异。ES5实现class的机制其核心就是在属性描述符,核心ES5的API就是:(**重点**)
1 Object.getOwnPropertyNames(Obj.prototype);//获取对象的属性名称列表 2 Object.getOwnPropertyDescriptor(Obj.prototype,time);//获取属性描述符对象 3 Object.defineProperty(InheritObj.prototype,time,attrObj);//给对象的属性绑定属性描述符对象
三、私有属性与方法、静态属性与方法
2.1静态属性与方法
在class语法中除了引入继承的特性,还实现了静态属性和静态方法的特性,在ES提案中的私有属性和方法也有比较多的支持者。目前,私有属性还是基于现有的语法特性,然后开发者们按照约定熟成的方式来实现,但并不能完全实现属性和方法的私有性。
1 //使用static指令实现静态属性和静态方法 2 class Foo{ 3 static a ; 4 static b = 10; 5 static c = function c(){ 6 console.log(this.a + this.b); 7 } 8 } 9 class Fun extends Foo{ 10 constructor(){ 11 super() 12 } 13 } 14 Foo.a = 18; 15 Foo.c();//28 16 Fun.a = 10; 17 Fun.c();//20 18 Foo.c();//28
通过上面的示例可以看到,在JavaScript中的class机制同样实现了类的静态方法特性,被正常的继承,其实静态方法被继承相比原型属性和方法的继承还要简单,因为static声明的静态属性是可以枚举的,不需要复杂的获取实行描述符。所以,如果父级类的静态属性是引用值类型的话,就需要小心使用,因为class的继承机制仅仅只是将父级的引用值引用赋给了子类,所以它们可以共同修改这个引用值。
1 class Foo{ 2 static e = [1,2,3]; 3 } 4 class Fun extends Foo{ 5 constructor(){ 6 super() 7 } 8 } 9 console.log(Foo.e); //[1,2,3] 10 Fun.e.push(4); 11 console.log(Fun.e);//[1,2,3,4] 12 console.log(Foo.e);//[1,2,3,4]
这也是JavaScript类机制与像Java那些语言的根本区别,Java的继承是完全重新复制一份,互不干扰。但是如果要在JavaScript中实现深拷贝显然在性能上是个很大的损失。
一个额外的问题,直接在class中写静态属性并赋值的方式,是ES7的写法,在老版本的浏览器中不能识别(报错),我是用最新的chrome浏览器已经能识别。但是,目前来讲浏览器能不能识别并不重要,因为我们在使用ES6还是ES7的时候都会使用编译工具进行降级,在babel中我使用的7.5.5版本都不支持编译示例中的静态属性的语法,在编译的时候回报以下错误:
Support for the experimental syntax 'classProperties' isn't currently enabled (2:11): 1 | class Foo{ > 2 | static e = [1,2,3]; | ^ 3 | static bar(){ 4 | return "bar"; 5 | }
但同时,在后面babel也提示了能编译这个语法的插件:
Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
下载插件:
npm install @babel/plugin-proposal-class-properties --save-dev
然后在(.babel文件中配置,这种模式任选一种配置,这里我选择了宽松模式):
1 //严格模式 2 { 3 "plugins": ["@babel/plugin-proposal-class-properties"] 4 } 5 //宽松模式(我测代码时使用了这个) 6 { 7 "plugins": [ 8 ["@babel/plugin-proposal-class-properties", { "loose": true }] 9 ] 10 }
其实,bable编译后的代码非常的简单暴力:
Foo.e = [1, 2, 3];
2.2私有属性和方法:
这里有一篇作为参考:https://www.oschina.net/news/100766/tc39-approval-emcascript
关于私有方法的TC39提案:https://github.com/tc39/proposal-private-methods
TC39官网: https://tc39.es/,2019年1月通过的方案
私有属性示例:(使用同静态属性和方法的转码器插件:@babel/plugin-proposal-class-properties,安装和使用详细查看前面静态属性和方法的相关插件安装内容)
1 class Foo{ 2 #x = 10; 3 static poor(numVal){ 4 //这是会报错的,因为静态方法的this指向Foo,而私有属性的this指向实例对象 5 // 所以静态方法中不能使用私有属性 6 console.log(this.#x - numVal); 7 } 8 constructor(x = 0){ 9 this.num = this.#x; //公有属性可以获取私有属性的值 10 } 11 set x(value){ 12 this.#x +=value; //这里可以理解为公有方法内部给私有属性写入值 13 } 14 get x(){ 15 return this.#x; //这里可以理解为公有方法内部读取私有属性的值 16 } 17 sum(){ 18 console.log(this.num + this.#x); //这里可以理解为公有方法读取私有属性的值 19 } 20 } 21 class Fun extends Foo{ 22 constructor(){ 23 super() 24 } 25 y(){ 26 // 子类方法不能直接使用父类的私有属性,babel插件无法编译 27 // return this.#x; 28 } 29 } 30 var foo = new Foo(); 31 var fun = new Fun(); 32 console.log(fun.x);//10:这里可以理解为子类的实例对象,可以通过父类的公有方法获取父类的私有属性值 33 fun.x = 10; // 10+10: 这里会发生属性赋值遮蔽效果,如若是数组push就可以修改到真正的私有属性值 34 console.log(foo.x);//10: 类的实例可以通过类的公有方法获取类的私有属性值 35 console.log(fun.x);//20: 类的实例可以通过类的公有方法获取类的私有属性值 36 console.log(fun.num);//10:这个不能说明什么,因为是原始值类型赋值,然读取被赋值的公有属性(但是这个赋值发生在实例对象构造时期) 37 foo.x = 10;//10+10:这里可以理解为类的实例调用类的公有方法,给类的私有属性赋值 38 console.log(fun.x);//20:调用父类的方法读取父类的私有属性值 39 foo.sum();//10 + 20:这里同样是调用父类的方法打印父类的公有属性与父类的私有属性的数值之和
关于私有属性的语法:
- 声明私有属性直接使用#开头作为属性名,声明不能在私有属性前加this,但是使用时必须使用this调用
- 不能在子类调用父类的私有属性,但是可以在子类定义与父类同名的私有属性,并且互不干扰,但是我知道你不会这么做
- 不能在constructor中定义私有属性和方法
- 私有属性语法同样需要@babel/plugin-proposal-class-properties插件才能编译(ES7语法)
- 私有方法语法需要@babel/plugin-proposal-private-methods插件才能编译
关于私有属性和方法一直是困扰的问题,什么是私有,私有与静态的区别是什么?私有与公有又有什么区别?
其实可以这么开描述私有属性和方法:它不是静态的,也不是公有的属性和方法。这就好像哲学一样,我们可以用“关于数的学科”来描述数学,但是我们不能用“关于哲的学科”来描述哲学一样,私有属性和方法不能用类的私有属性和方法来理解它。
在程序中,静态属性和方法中的“静态”描述的是属性和方法相对于类是静止的,通俗的说法就是类在那里出现,静态属性和方法就在那里出现。所以静态属性和方法永远是被类名用(.)或者([key])的方式引用。
而公有属性和方法意思就是只有属于这个类的所有成员都能拥有,并非公共所有,而是公开所有,每个类的成员动能拥有的属性和方法。
注意,不管是静态的属性和方法,还是公有的属性和方法,都是相对类而言,相对类静止、相对类公开所有。静态就是相对类而言,类在那里出现,静态属性和方法才可以在那里出现;公有就是相对类是公开的,公开属性和方法只要是归属于类的对象就可以拥有,这个拥有是对象拥有一个独立的公开的属性和方法。
最后,私有同样也是相对类而言,与私有对立的就是公有,公有属性和方法有一个非常重要的隐式特性,就是对象拥有这个属性和方法的所有权限的意思,实例对象可以任意的将这个属性和方法赋值给别的对象,可以任意的读写属性和方法。而相对公有属性的私有属性,就是属于类的不公开所有的属性和方法(不能继承),也就是说类的实例对象没有私有属性和方法的所有权,类的实例对象不能对私有属性和方法进行读写。但是类的实例对象继承了类的公有方法,类在定义公有方法时,类自己可以将私有属性和方法用在任意自己公开的方法中。
所以,实例对象可以调用类定义的公开方法获取和修改私有属性,可以调用类的公开方法执行私有方法。实例对象一定要通过类的公有方法才能使用私有属性和方法。
私有方法:
1 class Foo{ 2 #a(){ 3 console.log("我是一个私有方法"); 4 } 5 n(){ 6 console.log("我是一个公有方法,我可以调用一个私有方法"); 7 this.#a() 8 } 9 } 10 11 var foo = new Foo(); 12 foo.n(); 13 foo.a();//foo.a is not a function 14 foo.#a();//这样导致无法编译
使用私有方法时,babel编译器还需要一个的插件:
npm install @babel/plugin-proposal-private-methods --save-dev
然后,再.babel文件中配置:
1 { 2 "presets":[ 3 "@babel/preset-env" 4 ], 5 "plugins": [ 6 ["@babel/plugin-proposal-class-properties", { "loose": true }], 7 ["@babel/plugin-proposal-private-methods", { "loose": true }] //引入支持编译私有方法的插件 8 ] 9 }
详细可以了解:https://babeljs.io/docs/en/babel-plugin-proposal-private-methods#via-cli
1 class Foo{ 2 #a(){ 3 console.log("我是一个私有方法"); 4 } 5 n(){ 6 console.log("我是一个公有方法,我可以调用一个私有方法"); 7 this.#a(); 8 } 9 } 10 class Fun extends Foo{ 11 constructor(){ 12 super() 13 } 14 m(){ 15 this.n(); 16 // this.#a();//无法编译,子类不能直接调用父类的方法 17 } 18 } 19 var foo = new Foo(); 20 var fun = new Fun(); 21 foo.n(); 22 fun.m();//子类可以调用包含私有方法的父类公有方法
四、修饰器(Decorator)
修饰器是面向切面编程思想的,用来修改类的行为的API。ES7引入这项功能,Babel转码器已经支持。在了解和使用修饰器之前,先来看一个案例需求和模拟实现:
需求一:模拟搜索引擎,输入搜索内容,点击搜索按钮实现搜索功能(不需要实际实现,只需要触发触发点击事件,打印出“向+urlA+发送请求+data")。
需求二:不改变搜所引擎基本功能(即不修改第一条需求的代码),当搜索功能被触发时,同时模拟实现记录搜索关键字的搜索次数(只需要将搜索数据发送到另一个url,即打印出“向+urlB+发送请求+data”)。
使用ES5的js语法基于面向切面的编程思想,实现以上需求:
1 <input type="text" name="" id="inp"> 2 <button id="but">搜索</button> 3 <script> 4 var inpDom = document.getElementById("inp"); 5 var butDom = document.getElementById("but"); 6 7 var keyValue = ""; 8 inpDom.oninput = function(){ 9 keyValue = this.value; 10 } 11 12 var requestFun = dealFun(getContent); 13 butDom.onclick = function(){ 14 requestFun(keyValue); 15 } 16 17 //模拟实现的搜索请求功能 18 function getContent(data){ 19 var url = "urlA" 20 console.log("向" + url + "发送请求搜索信息,数据:" + data); 21 } 22 23 //模拟实现的关键字记录请求功能,并应用面向切面编程思想代理搜索请求功能 24 function dealFun(func){ 25 return function(data){ 26 var url = "urlB"; 27 console.log("向" + url + "发送请求记录关键字,数据:" + data); 28 return func.apply(this,arguments); 29 } 30 } 31 </script>
测试效果:
1 向urlB发送请求记录关键字,数据:TC39 2 向urlA发送请求搜索信息,数据:CT39
使用ES7修饰器基于面向切面编程思想,实现模拟案例需求:
1 let inpDom = document.getElementById("inp"); 2 let butDom = document.getElementById("but"); 3 4 //实现搜索功能的类 5 class Search{ 6 constructor(){ 7 this.keyValue = ""; 8 } 9 #url = 'urlA'; 10 @dealFun 11 getContent(){ 12 console.log("向" + this.#url + "发送请求搜了信息,数据:" + this.keyValue); 13 } 14 } 15 //模拟实现关键字记录请求功能,基于ES7修饰器应用面向切面编程实现 16 function dealFun(proto,key,descriptor){ 17 let dealGetContent = descriptor.value; 18 descriptor.value = function(){ 19 let urlDeal = "rulB"; 20 console.log("向" + urlDeal + "发送请求记录关键字,数据:" + this.keyValue) 21 return dealGetContent.apply(this,arguments); 22 } 23 } 24 //实例化搜索对象 25 let oS = new Search(); 26 27 inpDom.oninput = function(){ 28 oS.keyValue = this.value; 29 } 30 butDom.onclick = function(){ 31 oS.getContent(); 32 }
测试效果:
向rulB发送请求记录关键字,数据:TC39
向urlA发送请求搜了信息,数据:TC39
ES7修饰器(Decorator)转码插件:
npm install --save-dev @babel/plugin-proposal-decorators
在.babelrc中配置装饰器:
1 { 2 "plugins": [ 3 ["@babel/plugin-proposal-decorators", { "legacy": true }], 4 ["@babel/plugin-proposal-class-properties", { "loose" : true }] 5 ] 6 }
修饰器配置手册:https://babeljs.io/docs/en/babel-plugin-proposal-decorators#legacy
一个修饰器问题:https://github.com/WarnerHooh/babel-plugin-parameter-decorator/issues/1
什么是修饰器?装饰器有什么功能?
修饰器就是基于现有的属性、方法、类,生成或替换一个全新的功能。可以给现有的属性重新配置属性描述符或重新赋值,甚至可以使用一个全新的值替换。由于类没有属性的描述符,但可以给类添加静态方法,给类重新赋值甚至继承和被继承等操作。先了解一些有必要理解的语法:
- 修饰器使用“@”开头命名,然后取非“@”部分的名称在类的外面生一个函数。
- 修饰器设置在需要装饰的类、属性、方法的上方,独立一行。
- 修饰器可以是方法,也可以是函数执行后放回的函数,函数执行返回函数就是将修饰器作为函数执行一样放到被装饰者的前面,可以传入参数,例如:@fun(data),这个修饰器执行需要放回一个方法用来装饰被装饰者。
- 修饰器不能装饰私有方法和属性,可以修饰静态属性方法和公开属性方法。
修饰器与属性:
1 class Search{ 2 @dealAttr 3 static attr = 10; 4 } 5 function dealAttr(proto,key,descriptor){ 6 // proto:属性所属对象--静态属性指向类;公有属性指向构造方法constructor 7 // key:属性名称 8 // descriptor:修饰对象--属性描述符+initializer; 9 // 可以给initializer配置一个函数,函数返回值会作用于属性的初始值 10 descriptor.value = 20;//通过属性描述符value定义属性初始值 11 }
修饰器与方法:
1 @dealSearch 2 class Search{ 3 @dealFun 4 getContent(){ 5 console.log("向" + this.#url + "发送请求搜了信息,数据:" + this.keyValue); 6 } 7 } 8 function dealFun(proto,key,descriptor){ 9 // proto:方法所属对象--公有属性指向构造方法constructor,静态属性指向类; 10 // key:方法名称 11 // descriptor:修饰对象--属性描述符 12 descriptor.valuer = function(){}//将方法指向一个全新的函数 13 }
修饰器与类:
1 @decorator 2 class A{} 3 function decorator(proto){ 4 //proto 指向类本身 5 return newClass; 6 } 7 //同等与 8 A = decorator(A) || A;
修饰器的表示方式:
@decorator //装饰器对应的函数作用于装饰对象 @decorator() //装饰器对应的函数执行后返回的函数作用于装饰对象