1,基本语法
之前: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
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。基本上,ES6的class
可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样
//定义类
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return '(' + this.x + ', ' + this.y + ')';
}
}
//上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。
//也就是说,ES5的构造函数Point,对应ES6的Point类的构造方法。
//Point类除了构造方法,还定义了一个toString方法。
//注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
//另外,方法之间不需要逗号分隔,加了会报错
console.log(typeof Point);// function
console.log(Point === Point.prototype.constructor);//true
//上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
//使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致
var p = new Point(1,2);
console.log(p.toString());//(1,2)
构造函数的
prototype
属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的
prototype
属性上面。
class Point{
constructor(){
}
toString(){
}
createNum(){
}
}
//等同于
Point.prototype = {
toString(){},
createNum(){}
}
//由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。
//Object.assign方法可以很方便地一次向类添加多个方法。
class Point{
constructor(){
}
}
Object.assign(Point.prototype,{
toString(){},
createNum(){}
});
//prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的
console.log(Point.prototype.constructor === Point);
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
class Point{
constructor(){
}
toString(){
}
}
console.log(Object.keys(Point.prototype));//[]
console.log(Object.getOwnPropertyNames(Point.prototype));//["constructor", "toString"]
//上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致
var Foo = function(x,y){
};//
Foo.prototype.toString = function(){
//
};
console.log(Object.keys(Foo.prototype));//["toString"]
console.log(Object.getOwnPropertyNames(Foo.prototype));//["constructor", "toString"]
//上面代码采用ES5的写法,toString方法就是可枚举的
//类的属性名,可以采用表达式
let meName = 'getNum';
class Creat{
constructor(len){
}
[meName](){
console.log(11)
}
}
//上面代码中,Square类的方法名getNum,是从表达式得到的
var create = new Creat();
create.getNum();//11
constructor方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加
//constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象
class Foo{
constructor(){
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo);//false
//上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类的实例对象
//生成类的实例对象的写法,与ES5完全一样,也是使用new命令。
//如果忘记加上new,像函数那样调用Class,将会报错。
//报错
var point = Point();
//正确
var point = new Point();
//与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),
//否则都是定义在原型上(即定义在class上)
//定义类
class Foo{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return '(' + this.x + ', ' + this.y + ')';
}
}
var f = new Foo(1,2);
console.log(f.toString());//(1,2)
console.log(f.hasOwnProperty('x'));//true
console.log(f.hasOwnProperty('y'));//true
console.log(f.hasOwnProperty('toString'));//false
console.log(f.__proto__.hasOwnProperty('toString'));//true
//x和y都是实例对象f自身的属性(因为定义在this变量上),
//所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Foo类上),
//所以hasOwnProperty方法返回false。这些都与ES5的行为保持一致。
//与ES5一样,类的所有实例共享一个原型对象
var f1 = new Foo(1,2);
var f2 = new Foo(2,3);
console.log(f1.__proto__ === f2.__proto__);//true
//p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的
//这也意味着,可以通过实例的__proto__属性为Class添加方法
f1.__proto__.conName = function(){
return 'andy';
};
console.log(f1.conName());//andy
console.log(f2.conName());//andy
//由于f1的原型就是f2的原型,因此f2也可以掉用这个方法,以及以后实例出来的对象,也可以调用;
//这就意味着,使用实例的__proto__属性修改原型,必须谨慎使用,另,不推荐使用;
不存在变量提升
//Class不存在变量提升(hoist),这一点与ES5完全不同
new Foo();//Uncaught ReferenceError: Foo is not defined
class Foo{
}
//上面代码中,Foo类使用在前,定义在后,这样会报错,因为ES6不会把类的声明提升到代码头部。
//这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。
{
let Foo = class {};
class Bar extends Foo {
}
}
//上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。
//但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,
//而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。
class表达式
//与函数一样,类也可以使用表达式的形式定义
const CreatNum = class Num{
getName(){
return Num.name;
}
};
//上面代码使用表达式定义了一个类。
//需要注意的是,这个类的名字是CreatNum而不是Num,Num只在Class的内部代码可用,指代当前类。
let init = new CreatNum();
console.log(init.getName());//Num
console.log(Num.name);//Uncaught ReferenceError: Num is not defined
//如果类的内部没用到的话,可以省略Num,也就是可以写成下面的形式
const CreatNums = class{
//
};
//采用Class表达式,可以写出立即执行的Class
let person = new class{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}('andy');
person.sayName();//andy
上面代码中,
person
是一个立即执行的类的实例
私有方法
私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现
//一种做法是在命名上加以区别
class Wig{
//公有方法
foo(bar){
this._baz(bar);
}
//私有方法
_baz(bar){
return this.sn = bar;
}
}
//_baz方法前面的下划线,表示这是一个只限于内部使用的私有方法。
//但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法
//另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的
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值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果
this的指向
//类的方法内部如果含有this,它默认指向类的实例。
//但是,必须非常小心,一旦单独使用该方法,很可能报错
class Logger{
printName(name = 'there'){
this.print(`hello ${name}`);
}
print(txt){
console.log(txt);
}
}
const logger = new Logger();
const {printName} = logger;
printName();//Uncaught TypeError: Cannot read property 'print' of undefined
//上面代码中,printName方法中的this,默认指向Logger类的实例。
//但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。
//一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
//另一种解决方法是使用箭头函数。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
//还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this
严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict
指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以ES6实际上把整个语言升级到了严格模式。
name属性由于本质上,ES6的类只是ES5的构造函数的一层包装,所以函数的许多特性都被Class
继承,包括name
属性
class Point {}
console.log(Point.name);
//name属性总是返回紧跟在class关键字后面的类名。
class的继承
extends
关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多
class ColorPoint extends Point{
}
//上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。
//但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类
class ColorP extends P{
constructor(x,y,color){
super(x,y);//调用父类的constructor(x,y)
this.color = color;
}
toString(){
return this.color + ' ' + super.toString();//调用父类的toString()
}
}
//上面代码中,constructor方法和toString方法之中,都出现了super关键字,
//它在这里表示父类的构造函数,用来新建父类的this对象
//子类必须在constructor方法中调用super方法,否则新建实例时会报错。
//这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Point{
}
class ColorPoint extends Point{
constructor(){
}
}
let p = new ColorPoint();//编译报错
//上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
//ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
//ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
//如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。
//也就是说,不管有没有显式定义,任何一个子类都有constructor方法
constructor(...args){
super(...args);
}
//另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
//这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
//上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的
类的prototype属性和__proto__属性
__proto__
属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和
__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性
class F{
}
class M extends F{
}
console.log(M.__proto__ === F);//true
console.log(M.prototype.__proto__ == F.prototype);//true
//Object.getPrototypeOf方法可以用来从子类上获取父类
console.log(Object.getPrototypeOf(B) === A);//true
//因此,可以使用这个方法判断,一个类是否继承了另一个类
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同
//第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数
//class A {}
//
//class B extends A {
// constructor() {
// super();
// }
//}
上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错
//
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,
因此super()在这里相当于A.prototype.constructor.call(this)
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A(); // A
new B(); // B
//上面代码中,new.target指向当前正在执行的函数。
//可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。
//也就是说,super()内部的this指向的是B
//作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错
class A {}
class B extends A {
m() {
super(); // 报错
}
}
//上面代码中,super()用在B类的m方法之中,就会造成句法错误
//第二种情况,super作为对象时,指向父类的原型对象
class F{
p(){
return 1;
}
}
class M extends F{
constructor(){
super();
console.log(super.p());//1
}
}
let m = new M();
//上面代码中,子类M当中的super.p(),就是将super当作一个对象使用。
//这时,super指向F.prototype,所以super.p()就相当于F.prototype.p()
//由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
class X{
constructor(){
this.p = 1;
}
}
class Y extends X{
get m(){
return super.p;
}
}
let y = new Y();
console.log(y.m);//undefined
//p是父类X实例的属性,super.p就引用不到它
//如果属性定义在父类的原型对象上,super就可以取到
class A{
}
A.prototype.x = 1;
class B extends A{
constructor(){
super();
console.log(super.x);//1
}
}
let b = new B();
//上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值
ES6 规定,通过
super
调用父类的方法时,
super
会绑定子类的
this
class X{
constructor(){
this.x = 1;
}
print(){
console.log(this.x);
}
}
class Y extends X{
constructor(){
super();
this.x = 2;
}
m(){
super.print();
}
}
let y = new Y();
y.m();//2
//上面代码中,super.print()虽然调用的是X.prototype.print(),
//但是X.prototype.print()会绑定子类Y的this,导致输出的是2,而不是1。
//也就是说,实际上执行的是super.print.call(this)
//由于绑定子类的this,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
class A{
constructor(){
this.x = 1;
}
}
class B extends A{
constructor(){
super();
this.x = 2;
super.x = 3;
console.log(super.x);//undefined
console.log(this.x);//3
}
}
let b = new B();
//上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。
//而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
注意,使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
实例的__proto__属性
//子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。
//也就是说,子类的原型的原型,是父类的原型
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
//上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型
//因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
//上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1
原型构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承。class的取值函数(getter)和存值函数(setter)
get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为class X{
constructor(){
}
get prop(){
return 'getter';
}
set prop(val){
console.log('setter:'+ val);
}
}
let inst = new X();
inst.prop = 123;// setter:123
console.log(inst.prop);//getter
//上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了
//存值函数和取值函数是设置在属性的descriptor对象上的
class的Generator方法
//如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数
class F{
constructor(...args){
this.args =args;
}
*[Symbol.iterator](){
for(let arg of this.args){
yield arg;
}
}
}
for(let x of new F('hello','andy')){
console.log(x);
}
//hello
//andy
//F类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。
//Symbol.iterator方法返回一个F类的默认遍历器,for...of循环会自动调用这个遍历器。
class的静态方法
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”class F{
static className(){
return 'hello';
}
}
console.log(F.className());//hello
var f = new F();
//console.log(f.className());//Uncaught TypeError: f.className is not a function
//上面代码中,F类的classMethod方法前有static关键字,表明该方法是一个静态方法,
//可以直接在F类上调用(Foo.classMethod()),而不是在F类的实例上调用。
//如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法
//父类的静态方法,可以被子类继承
class M{
static className(){
return 'hello';
}
}
class B extends M{
}
console.log(B.className());//hello
//静态方法也是可以从super对象上调用的
class A{
static className(){
return 'hello';
}
}
class C extends A{
static className(){
return super.className() + ',andy'
}
}
console.log(C.className());//hello,andy
class的静态属性和实例属性
//静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性
class F{
}
F.prop= 1;
console.log(F.prop);//1
//上面的写法为F类定义了一个静态属性prop
//目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性
//以下两种写法无效
class M{
//1
prop:1
//2
static prop:1
}
console.log(M.prop);//undefined
//ES7有一个静态属性的提案,目前Babel转码器支持
//这个提案对实例属性和静态属性,都规定了新的写法
//(1)类的实例属性
//类的实例属性可以用等式,写入类的定义之中
//(2)类的静态属性
//类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。
new.target属性
new
是从构造函数生成实例的命令。ES6为new
命令引入了一个new.target
属性,(在构造函数中)返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的