前言
在这个疫情慢慢好转,各种活动渐渐开始的时候,听到对象这个词,会让人浮想联翩。 只想不做,不是我们烧烤摊的风格,所以还是时间管理一波,总结下所想到的东西吧。
烤的吃腻了,来点小清新吧!
一个问题
在一个旧的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', {
configurable: false,
enumerable: false,
writable: false,
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', {
get: function () {
return _a++;
},
});
console.log(a === 1 && a === 2); // true
还有一个挺有趣的,是关于宽松等的a == 1 && a == 2
,这个问题大家可以想想,其实是靠隐式类型转换。
跑题了,让我们回到主题。
new
关于new操作符,我们需要大致了解下做了什么事情。调用new,其实是做了以下四个步骤。
- 创建一个新对象。
- 将构造函数的作用域赋给新对象。
- 执行构造函数中的代码。
- 返回新对象。
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属性
这里属性并不是规范中的属性,而且浏览器厂商实现的,不过现在基本主流浏览器都实现了该属性。
那这个属性有什么作用呢。这里总结了下有两个:
- 作用域链延长。
- 建立对象与函数联系。
// 作用域链延长
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,
enumerable: true,
configurable: true,
writable: true,
});
} 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',
value: function 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, writable: true, configurable: true },
});
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(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
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.construct) return false;
if (Reflect.construct.sham) return 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(this, arguments);
}
return B;
})(A);
这里代码一丢丢多,就不细讲了,主要关注一下**_inherits函数和_createSuper函数**就行。可以看到_inherits主要做的是将subClass.prototype属性指向了一个基于superClass.prototype创建的对象,而_createSuper主要做的是创建一个super函数,并将构造函数中的属性复制到实例上。
这里还有一点比较特殊,就是使用class语法还会导致构造函数的继承,即subClass.__proto__ = superClass
,这样会导致在构造函数上定义的静态属性也是可继承的。
总结
戛然而止有点不好,还是来个总结吧。
最开始的例子报错的问题大家也应该能理解了,主要是因为React那种常用的定义函数的方法是将方法定义在了实例上。而在子类中,通过super调用的方法是父类原型上的方法,所以就会导致文章开头的错误。
最后
如果觉得文章对您有帮助的话,就麻烦顺手点个赞吧👍~!
关注「前端烧烤摊」, 第一时间获取优质文章。