你不知道的JavaScript对象

2,380 阅读15分钟

前言

在这个疫情慢慢好转,各种活动渐渐开始的时候,听到对象这个词,会让人浮想联翩。 只想不做,不是我们烧烤摊的风格,所以还是时间管理一波,总结下所想到的东西吧。

烤的吃腻了,来点小清新吧!

一个问题

在一个旧的react项目中,00后贡献的代码基本都是Function Component + React Hook,但是一些祖传模块中,使用的还是class组件。

之前,组件逻辑的复用,我们会考虑使用HOC或render props,但是还有一个较少人使用的方式,那就是继承(对于类来说,继承是一个复用代码的方式,而我们的组件使用的也是class,所以使用继承来复用组件代码是不是也合情合理?)。

举个🌰

啥也别说,先来段父子关系。

情景带入:父亲是个工作狂,生活就只有工作。对于儿子而言,家里通过父亲努力的工作,已经奔小康了,所以儿子可以享受生活,工作之余还可以娱乐娱乐。

class SuperComponent extends React.Component {
    life = () => {
        console.log("Life's a struggle!");
    };
}

class ChildComponent extends SuperComponent {
    life = () => {
        console.log('Life is eating, drinking and having fun!');
        super.life();
    };
}
这里使用了在react中常用的使用箭头函数来定义方法。

不对劲?

大家看这个栗子,觉得有什么问题吗?

大家可能会说,你这个栗子三观不正,爸爸在struggle而儿子却在eat eat drink drink。当然这或许是个问题,但不是今天要谈的。

这里的问题是,执行这个儿子组件的生活方法时,这个家庭会毁掉。

const child = new ChildComponent();
child.life();

思考

难道这里的意思是,儿子吃吃喝喝,爸爸的生活就不能过下去了?

在思考这种人生问题之前,大家不妨想一下,为什么我们每天都在用的class突然不香了,我们真的懂class语法吗?

创建对象的几种方式

在讨论这个问题之前,我们先来简单了解下创建对象历史。

字面量方式

var xiaohu = {
    name'李元浩',
    getName() {
        return this.name;
    },
};

var xiaoming = {
    name'史森明',
    getName() {
        return this.name;
    },
};

通过字面量,在js中我们可以很方便就创建一个对象。但是对于批量创建这种对象时,会稍显麻烦。所以就有了下面的工厂模式模式。

工厂模式

function createPerson(name) {
    return {
        name,
        getName() {
            return this.name;
        },
    };
}
var persons = ['xiaohu''xiaoming'].map((name) => createPerson(name));

这种方式也有问题,就是我们不知道我们的对象是从哪个类(模版)创建出来的。使用下面的构造函数模式可以解决这个问题。

构造函数模式

function Person(name) {
    this.name = name;
    this.getName = function () {
        return this.name;
    };
}
const p = new Person('xiaohu');
console.log(p instanceof Person); // true

这样创建的对象就可以使用instanceof操作符来确认是哪个类的实例了。那是不是这样就完美了呢?这样其实还有问题,那就是构造函数中定义的方法是实例方法,每个实例中的方法干了同样的事但却不是同一个的方法。

也就是说,每个实例中的方法都独自占用一份内存。

const p1 = new Person('xiaohu');
const p2 = new Person('xiaoming');
console.log(p1.getName === p2.getName); // false

使用原型可以解决这个问题。

原型模式

function Person(name) {
    this.name = name;
}
Person.prototype.getName = function () {
    return this.name;
};
var p1 = new Person('xiaohu');
var p2 = new Person('xiaoming');
console.log(p1.getName === p2.getName); // true

对于我们定义的每个函数,解释器都会在背后帮我们定义个原型对象,我们可以在这个对象上定义我们的方法,这时就等于把公共方法定义在了某个特定作用域而不定义在实例上。

但是这种方式,让人看起来始终感觉怪怪的,说到底就是缺少一个class语法,而在ES6中这个问题得到了解决。

class语法

class Person {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

有了class语法糖,感觉一切突然香了起来。

这个话题就聊到这里,当然还有别的模式,大家有兴趣可以去翻翻书。

几个重要概念

下面,还需要再来聊几个重要的概念。

对象属性

对象属性分为两种,一种叫数据属性,另一种叫访问器属性。每种属性都有对应的配置项,用于对属性进行进一步的控制。

  • 数据属性

    • configurable: 能否删除、能否修改特性、能否修改为数据属性。
    • enumerable: 能否使用for-in循环返回属性。
    • writable:能否修改属性值。
    • value:属性值。
  • 访问器属性

    • configurable
    • enumerable
    • get:读取属性时调用的函数
    • set:写入属性时调用的函数
// 数据属性
var A = {};
Object.defineProperty(A, 'a', {
    configurablefalse,
    enumerablefalse,
    writablefalse,
    value'a',
});
delete A.a; // 无效
A.a = 'aa';
console.log(A.a); // a(无法设置值)
// 访问器属性
var A = {};
Object.defineProperty(A, 'a', {
    get: function() {
        // 在取属性a的值时调用
    },
    set: function(value) {
        // 在给属性a赋值时调用
    }
})

关于访问器属性,这里多提一下,源于之前遇到的一道面试题。

如何让a === 1 && a === 2。

这里只要记得到访问器属性就能解出这道题。

var _a = 1;
Object.defineProperty(window'a', {
    getfunction () {
        return _a++;
    },
});
console.log(a === 1 && a === 2); // true

还有一个挺有趣的,是关于宽松等的a == 1 && a == 2,这个问题大家可以想想,其实是靠隐式类型转换。

跑题了,让我们回到主题。

new

关于new操作符,我们需要大致了解下做了什么事情。调用new,其实是做了以下四个步骤。

  1. 创建一个新对象。
  2. 将构造函数的作用域赋给新对象。
  3. 执行构造函数中的代码。
  4. 返回新对象。
function new(func) {// func是构造函数
    var obj = {};
    obj.__proto__ = func.prototype;
    var res = func.apply(obj, Array.prototype.slice.call(arguments, 1));
    return typeof res === 'object' && res !== null ? res : obj;
}

这里可以重点看看第2、3行,返回的对象有个__proto__属性指向了构造函数的原型prototype,并且把返回对象传入了构造函数中指向并绑定this,所以构造函数中定义的属性(this.xxx)最后会定义在返回对象上。

proto属性

这里属性并不是规范中的属性,而且浏览器厂商实现的,不过现在基本主流浏览器都实现了该属性。

那这个属性有什么作用呢。这里总结了下有两个:

  1. 作用域链延长。
  2. 建立对象与函数联系。
// 作用域链延长
var A = {};
var B = { b'b' };
A.__proto__ = B;
console.log(A.b); // b

这里可以看到,通过使用__proto__属性连接后,对象变量的查找会沿着这条链(原型链)去查询。

// 建立对象与函数的联系
function A() {}
var a = {};
a.__proto__ = A.prototype;
console.log(a instanceof A); // true

这里可以看到,将对象的__proto__属性指向函数的原型后,该对象就被认为是对应函数的原型了(instanceof生效了),这里也是为什么new出来的对象可以用instanceof判断是哪个类的对象了。

原型链

js中的继承,其实是基于原型链去实现的。

function Super() {
    this.a = 'a';
}
Super.prototype.getA = function () {
    return this.a;
};
const s = new Super();

先来看看上面的代码背后发生了什么?

从这里可以看到,我们根据构造函数new出来的对象与构造函数是没有直接联系的,而是通过原型这个中间对象来进行连接。

那如何基于原型去继承呢?

这里假设我们有两个构造函数,分别是Super和Sub。

我们只要让Super的原型(Super.prototype)和Sub的原型(Sub.prototype)链接起来,之后new Sub()创建的对象就可以通过原型链连接起来了。

需要注意的是,我们这里不能将Sub.prototype直接指向Super.prototype(因为这样的话会丢失掉Super中定义的属性),而是需要将Sub的原型替换成Super的实例。

用代码来表示就是

function Super() {}
function Sub() {}
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

剖析class语法糖

现在终于可以来说说跟文章开头问题相关的问题了——class语法糖。

还是一样,先来看个栗子。

class A {
    constructor() {
        this.a = 'a';
    }
    b = 'b';
    getA() {}
    getB = () => {};
}

这里的a和b,getA和getB有啥区别?

babel转译一下

我们这里借助babel转译一下,看看class语法糖是如何实现的。

'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,
            enumerabletrue,
            configurabletrue,
            writabletrue,
        });
    } else {
        obj[key] = value;
    }
    return obj;
}

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

        _defineProperty(this'b''b');

        _defineProperty(this'getB'function () {});

        this.a = 'a';
    }

    _createClass(A, [
        {
            key'getA',
            valuefunction getA() {},
        },
    ]);

    return A;
})();

通过上面的介绍,看这些转译后的代码应该没啥大问题了。

这里主要关注一下**_defineProperty函数_createClass函数**。

_defineProperty

_defineProperty其实也只是在this上添加属性,与在constructor中定义属性是一致的。

也就是说下面这两种表示是等价的,都是实例属性:
class A {
    constructor() {
        this.a = 'a';
    }
}

class A {
    a = 'a';
}

_createClass

可以看到_createClass中调用了_defineProperties,而_defineProperties中也是调用了Object.defineProperty来定义属性的,不过需要注意的是,这里的target是构造函数的原型对象_defineProperties(Constructor.prototype, protoProps);

也就是说,这种方式定义的属性是在原型上的:
class A {
    getA() {}
}

继承

接下来来看看继承。

源码。

class A {}
class B extends A {}

转译后代码。

'use strict';

function _typeof(obj) {
    '@babel/helpers - typeof';
    if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj &&
                typeof Symbol === 'function' &&
                obj.constructor === Symbol &&
                obj !== Symbol.prototype
                ? 'symbol'
                : typeof obj;
        };
    }
    return _typeof(obj);
}

function _inherits(subClass, superClass) {
    if (typeof superClass !== 'function' && superClass !== null) {
        throw new TypeError(
            'Super expression must either be null or a function'
        );
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: { value: subClass, writabletrueconfigurabletrue },
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    _setPrototypeOf =
        Object.setPrototypeOf ||
        function _setPrototypeOf(o, p) {
            o.__proto__ = p;
            return o;
        };
    return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
    var hasNativeReflectConstruct = _isNativeReflectConstruct();
    return function _createSuperInternal() {
        var Super = _getPrototypeOf(Derived),
            result;
        if (hasNativeReflectConstruct) {
            var NewTarget = _getPrototypeOf(this).constructor;
            result = Reflect.construct(SuperargumentsNewTarget);
        } else {
            result = Super.apply(thisarguments);
        }
        return _possibleConstructorReturn(this, result);
    };
}

function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
        return call;
    }
    return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError(
            "this hasn't been initialised - super() hasn't been called"
        );
    }
    return self;
}

function _isNativeReflectConstruct() {
    if (typeof Reflect === 'undefined' || !Reflect.constructreturn false;
    if (Reflect.construct.shamreturn false;
    if (typeof Proxy === 'function'return true;
    try {
        Date.prototype.toString.call(
            Reflect.construct(Date, [], function () {})
        );
        return true;
    } catch (e) {
        return false;
    }
}

function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf
        ? Object.getPrototypeOf
        : function _getPrototypeOf(o) {
              return o.__proto__ || Object.getPrototypeOf(o);
          };
    return _getPrototypeOf(o);
}

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

var A = function A() {
    _classCallCheck(this, A);
};

var B = /*#__PURE__*/ (function (_A) {
    _inherits(B, _A);

    var _super = _createSuper(B);

    function B() {
        _classCallCheck(this, B);

        return _super.apply(thisarguments);
    }

    return B;
})(A);

这里代码一丢丢多,就不细讲了,主要关注一下**_inherits函数_createSuper函数**就行。可以看到_inherits主要做的是将subClass.prototype属性指向了一个基于superClass.prototype创建的对象,而_createSuper主要做的是创建一个super函数,并将构造函数中的属性复制到实例上。

这里还有一点比较特殊,就是使用class语法还会导致构造函数的继承,即subClass.__proto__ = superClass,这样会导致在构造函数上定义的静态属性也是可继承的。

总结

戛然而止有点不好,还是来个总结吧。

最开始的例子报错的问题大家也应该能理解了,主要是因为React那种常用的定义函数的方法是将方法定义在了实例上。而在子类中,通过super调用的方法是父类原型上的方法,所以就会导致文章开头的错误。

最后

如果觉得文章对您有帮助的话,就麻烦顺手点个赞吧👍~!

关注「前端烧烤摊」, 第一时间获取优质文章。