JavaScript面向对象系列之关于 ES6 class 的细节

463 阅读6分钟

ES6 class 该注意的细节

区分函数的多种的角色

// 函数/构造函数
function Fn(name) {
    this.x = 100;
    this.name = name;
}
// 作为原型,和「类/实例」有关
Fn.prototype.y = 200;
Fn.prototype.getX = function() {};
Fn.prototype.getName = function() {};
// 作为对象,和前面没有直接的关系
Fn.x = 1000;
Fn.getX = function() {};

Fn.getX(); //对象的成员访问
new Fn().getX(); //实例上的getX()方法
Fn(); //当做普通函数执行,返回undefined

上面是依据ES5的形式创造一个类

ES6中可以使用 class 关键字创建类

class Fn {}
Fn();

报错, Uncaught TypeError: Class constructor Fn cannot be invoked without 'new'

基于 class 声明的构造函数必须基于 new 执行,不允许当做普通函数执行

如何在es5中模拟这种判断?

如果是 new 执行,那么当前 this 一定是当前构造函数的实例,如果不是构造函数的实例,就说明不是 new 执行,就抛出错误

function Sum() {
    if (!(this instanceof Sum)) throw new TypeError('constructor Sum cannot be invoked without new');
    console.log('OK');
}
// Sum();//报错
new Sum();

构造函数

class Fn {
    // 构造函数体
    constructor(name) {
        // this -> 创造的实例对象 「this.xxx=xxx设置的私有属性」
        // this.x = 100;
        this.name = name;
        //如果返回对象,以返回的为主,否则返回实例对象
    }
}

constructor 就是其构造函数体

私有属性简易写法

class Fn {
    constructor(name) {
        this.name = name;
    }
    x = 100; //等价于构造函数体中的“this.x=100”
}

但是构造函数中如果有了 this.x=xxx ,那么以构造函数中的最终赋值为准

原型上的内容设置

class不能在原型上设置属性,只能设置函数,为什么会有这个特性,一会再说

class Fn {
    constructor() {}
    y = 200; //等价于构造函数体中的“this.x=100”,并不是在原型上设置属性y
    getX() {} //这里的方法是设置在原型上的
    getName() {}
}
Fn.prototype.y = 200

用这种方法设置在原型上的函数是没有 prototype 的,类似于: obj={fn(){}}

如果想在原型上设置属性,只能用原来的方法写: Fn.prototype.y = 200

image。png

将类看做普通对象,设置私有属性

将类看做普通对象添加属性和方法,在 class 中,叫做静态私有属性和方法

class Fn {
    constructor() {}
    y = 200; //等价于构造函数体中的“this.x=100”,并不是在原型上设置属性y
    getX() {} //这里的方法是设置在原型上的
    getName() {}

    static x = 1000
    static getX() {}
}

image。png

以上就是如何通过es6创造一个类的过程

区分原型上和实例上的方法

class Fn {
    x = 100;
    getX = function() {}
    getY = () => {}

    getZ() {
        console.log(this)
    }
}

注意: xgetXgetY 都属于一类,最后在实例的私有属性上,没有不同。而 getZ 这种写法是声明在原型上的

const f = new Fn()
f.getZ()
Fn.prototype.getZ()
f.getZ.call(10)

所以 getZ 函数有可能 f.getZ() 执行,或者 Fn.prototype.getZ() 这样执行,或者 call 执行,其 this 的指向会根据不同情况改变

image。png

那么如何让其中的 this 不随便被改变而永远指向当前实例呢?答案是使用箭头函数的写法

class Fn {
    getX1 = () => {
        console.log(this)
    }
}
const f = new Fn()
f.getX1()
f.getX1.call(10)

因为箭头函数没有 this ,其中的 this 是函数上下文中的 this ,这个 this 都是 Fn 的实例,因为在 new 的时候,都将构造函数内部 this 指向了返回的实例对象。

image。png

class Fn {
    getX: function() {}
    getY: () => {}
}

以上写法将报语法错误。不支持 : 写法。

在react,以上特性有这样的应用:

image。png

点击按钮,输出的是 undefined

分析如下:

  1. onClick={this.handle}这里handle前面的this一定生成组件时候的实例,因为这个this是在render函数当中的,最后渲染组件的时候,一定是实例.render()的执行方式,所以这里的this一定是组件实例
  2. onClick={this.handle}最后会被转化为事件监听的形式,例如button.addEventListener('click',handle)。最后触发点击事件的时候。一定是以回调函数的形式触发,那么其中的handle(){console.log(this)}这个函数中的this在严格模式下是undefined,非严格模式下是window,所以上面这段代码输出undefined

那我想让 handlethis 永远是实例,如何写呢?

使用箭头函数即可

handle = () => {
    console.log(this)
}

此时 handle 并不是原型上的函数了,而是私有属性。并且因为其是箭头函数,所以 this是当前创建函数所在作用域上的this(当前创建函数的上下文的this),即外层的this,外层的thisnew执行的时候,已经被绑定成了当前实例,所以此时this永远是当前实例.

还有一些方法可以规避这个问题,具体的可以看文档react-事件处理

使用Babel查看 class 到底做了什么

ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

我们打开Babel,将下面calss关键字语法糖代码转化为ES5,看看class语法糖到底是如何写的

class Fn {
    constructor() {
        this.name = 1
    }
    name = 2
    static name = 3
}

结果为:

"use strict";

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

function _defineProperty(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

var Fn = function Fn() {
    _classCallCheck(this, Fn); //判断是否用new执行,如果没有用new ,那就报错

    _defineProperty(this, "name", 2); //先执行name=2,给实例赋值为2,并且有重名,会覆盖赋值

    this.name = 1; //在执行构造函数
};

_defineProperty(Fn, "name", 3); //给构造函数添加属性,相当于静态属性

所以,构造函数中的 this.name=1 后执行, name = 2 先执行

class Fn {
    constructor() {
        this.name = 1
    }
    name = 2
    name = 4
    static name = 3

    getName() {
        return this.name
    }
    getX() {
        return this.x
    }
    static getY() {
        return 'static getY'
    }
}

转化后:

"use strict";

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;
}

function _defineProperty(obj, key, value) {
    if (key in obj) {
        Object.defineProperty(obj, key, {
            value: value,
            enumerable: true,
            configurable: true,
            writable: true
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

var Fn = /*#__PURE__*/ function() {
    function Fn() {
        _classCallCheck(this, Fn);

        _defineProperty(this, "name", 2);

        _defineProperty(this, "name", 4);

        this.name = 1;
    }

    _createClass(Fn, [{
        key: "getName",
        value: function getName() {
            return this.name;
        }
    }, {
        key: "getX",
        value: function getX() {
            return this.x;
        }
    }], [{
        key: "getY",
        value: function getY() {
            return 'static getY';
        }
    }]);

    return Fn;
}();

_defineProperty(Fn, "name", 3);

加入实例方法和静态方法后,发现其多了一个 _createClass 函数,其中的逻辑是按照传入的参数的顺序不同 ,只是简单的分别向原型对象构造函数上添加方法,分别成为实例的共有方法和构造函数上的方法。

注意:

看了转换后就解释一个我思考过的疑问:为啥没法向原型上绑定静态的变量,而只能区分原型上的共有方法和构造函数上的静态方法呢?

因为在转换代码里 constructor(){this.name=1} 和直接写 name = 1 是一样的作用,最终被转换成了相同意义的代码,而 getName(){return this.name}getName=()=>{return this.name} 则是不同逻辑,不同意义的代码

举例如下:

class Fn {
    constructor() {
        this.name = 1
    }
    name = 2
    name = 3
    static name = 4

    getName() {
        return this.name
    }
    static getStaticName() {
        console.log(this)
        return this.name
    }
}

const x = new Fn()
x.getName() //返回1
x.getStaticName() //报错,x.getStaticName is not a function
Fn.getStaticName() //打印class Fn ,返回4

image。png

class Fn{
  getA = ()=>{}
  getB = function(){}
  getC(){}
  
  static getD = ()=>{}
  static getE = function(){}
  static getF(){}
}
"use strict";

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; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var Fn = /*#__PURE__*/function () {
  function Fn() {
    _classCallCheck(this, Fn);

    _defineProperty(this, "getA", function () {});

    _defineProperty(this, "getB", function () {});
  }

  _createClass(Fn, [{
    key: "getC",
    value: function getC() {}
  }], [{
    key: "getF",
    value: function getF() {}
  }]);

  return Fn;
}();

_defineProperty(Fn, "getD", function () {});

_defineProperty(Fn, "getE", function () {});

image.png

image.png

将es5创造类的语法转化为es6的class语法

 function Modal(x, y) {
     this.x = x;
     this.y = y;
 }
 Modal.prototype.z = 10
 Modal.prototype.getX = function() {
     console.log(this.x);
 }
 Modal.prototype.getY = function() {
     console.log(this.y);
 }
 Modal.n = 200
 Modal.setNumber = function(n) {
     this.n = n;
 }

 let m = new Model(10, 20);
 class Modal {
     constructor(x, y) {
         this.x = x;
         this.y = y;
     }
     getX() {
         console.log(this.x);
     }
     getY() {
         console.log(this.y);
     }
     static n = 200;
     static setNumber(n) {
         this.n = n;
     }
 }
 Modal.prototype.z = 10;
 let m = new Model(10, 20);

一些关于面向对象的代码分析(加深理解)

function Fn() {
    let a = 1;
    this.a = a;
}
Fn.prototype.say = function() {
    this.a = 2;
}
Fn.prototype = new Fn;
let f1 = new Fn;
Fn.prototype.b = function() {
    this.a = 3;
};
console.log(f1.a);
console.log(f1.prototype);
console.log(f1.b);
console.log(f1.hasOwnProperty('b'));
console.log('b' in f1);
console.log(f1.constructor == Fn);

image。png

解析:

  1. 声明 Fn 并,声明 Fn.prototype.say

image。png

  1. Fn.prototype = new Fn; 分为两步

image。png

image。png

关系图有了之后,输出一目了然

小tips:检测某个属性是否为当前对象的属性

  • in :不论是私有还是公有属性(原型链),只要有结果就是 true

  • hasOwnProperty :检测是否为对象的私有属性,只要私有中没有这个属性,结果是 false

检测当前属性是否为对象的公有属性

function hasPubProperty(obj, attr) {
    return (attr in obj) && !obj.hasOwnProperty(attr);
}

这个方法局限性:如果私有中有这个属性,公有也有,此方法检测是不准确的

封装一个检测公有属性的方法(仅存在它的原型或者原型链上),

方法一:原理是根据原型链,一层一层往上找

Object.prototype.hasPubProperty = function hasPubProperty(attr) {
    // this -> obj
    let self = this,
        prototype = Object.getPrototypeOf(self); //获取原型
    while (prototype) { //一直找到原型没有的时候停止循环
        if (prototype.hasOwnProperty(attr)) { //如果在原型上有私有属性
            return true;
        }
        prototype = Object.getPrototypeOf(prototype); //没有的话继续往上
    }
    return false;
};

方法二: in操作符检测的特点:先看自己私有中是否有,如果没有会默认按照原型链一层层查找

Object.prototype.hasPubProperty = function hasPubProperty(attr) {
    let self = this,
        prototype = Object.getPrototypeOf(self);
    return attr in prototype;
};