打算以一名【合格】前端工程师的自检清单为纲整理自己的知识体系,由于是整理,风格偏简洁,勘误、疑惑、意见建议可前往小弟博客交流,后续的整理也会在博客及时更新,博客地址github.com/logan70/Blo…。
原型设计模式以及JavaScript中的原型规则
原型与原型链
原型
JavaScript中,一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象
就是原型。
获取原型的方法
- 可以通过
Object.getPrototypeOf(obj)
来获取obj
的原型。 - 当然对象都是通过构造函数
new
出来的(字面量对象也可以这么理解),也可以通过访问对应构造函数的prototype
属性来获取其原型。
const obj = {}
expect(Object.getPrototypeOf(obj) === Object.prototype).toBe(true)
原型链
当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined
。这条由对象及其原型组成的链就叫做原型链。
特殊原型规则
原型链顶层
普通对象可以理解为Object
构造函数创建的,即普通对象的原型都指向Object.prototype
,Object.prototype
也是个对象,但是其原型比较特殊,为null
,是原型链的顶层,切记!!!
expect(Object.getPrototypeOf({})).toBe(Object.prototype)
expect(Object.getPrototypeOf(Object.prototype)).toBe(null)
构造函数的原型
函数,包括构造函数都可理解为由构造函数Function
创建,Function
本身也不例外。
const getProto = Object.getPrototypeOf
const FuncProto = Function.prototype
expect(getProto(Function)).toBe(FuncProto)
expect(getProto(Object)).toBe(FuncProto)
expect(getProto(Number)).toBe(FuncProto)
expect(getProto(Symbol)).toBe(FuncProto)
expect(getProto(Array)).toBe(FuncProto)
instanceof的底层实现原理及手动实现
作用
instanceof
用于检测右侧构造函数的原型是否存在于左侧对象的原型链上。
Symbol.hasInstance
ES6
新增的内置Symbol,用作对象方法标识符,该方法用于检测任意对象是否为拥有该方法对象的实例。instanceof
操作符优先使用该Symbol
对应的属性。
这样一来instanceof
右侧并非必须为函数,对象也可以的。示例代码如下:
const MyArray = {
[Symbol.hasInstance](obj) {
return Array.isArray(obj)
}
}
expect([] instanceof MyArray).toBe(true)
手写实现
const isObj = obj => ((typeof obj === 'object') || (typeof obj === 'function')) && obj !== null
function myInstanceOf(instance, Ctor) {
if (!isObj(Ctor)) // 右侧必须为对象
throw new TypeError('Right-hand side of 'instanceof' is not an object')
const instOfHandler = Ctor[Symbol.hasInstance]
// 右侧有[Symbol.hasInstance]方法,则返回其执行结果
if (typeof instOfHandler === 'function') return instOfHandler(instance)
// 右侧无[Symbol.hasInstance]方法且不是函数的,返回false
if (typeof Ctor !== 'function') return false
// 左侧实例不是对象类型,返回false
if (!isObj(instance)) return false
// 右侧函数必须有原型
const rightP = Ctor.prototype
if (!isObj(rightP))
throw new TypeError(`Function has non-object prototype '${String(rightP)}' in instanceof check`)
// 在实例原型连上查找是否有Ctor原型,有则返回true
// 知道找到原型链顶级还没有,则返回false
while (instance !== null) {
instance = Object.getPrototypeOf(instance)
if (instance === null) return false
if (instance === rightP) return true
}
}
ECMAScript定义
标准出处 -> ECMAScript#instanceof
InstanceofOperator ( V, target )
- If Type(target) is not Object, throw a TypeError exception.
- Let instOfHandler be ? GetMethod(target, @@hasInstance).
- If instOfHandler is not undefined, then
- Return ToBoolean(? Call(instOfHandler, target, « V »)).
- If IsCallable(target) is false, throw a TypeError exception.
- Return ? OrdinaryHasInstance(target, V).
OrdinaryHasInstance ( C, O )
- If IsCallable(C) is false, return false.
- If C has a [[BoundTargetFunction]] internal slot, then
- Let BC be C.[[BoundTargetFunction]].
- Return ? InstanceofOperator(O, BC).
- If Type(O) is not Object, return false.
- Let P be ? Get(C, "prototype").
- If Type(P) is not Object, throw a TypeError exception.
- Repeat,
- Set O to ? O.[[GetPrototypeOf]]().
- If O is null, return false.
- If SameValue(P, O) is true, return true.
实现继承的方式及优缺点
面向对象编程
面向对象编程(Object Oriented Programming),简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
面向对象编程的三大基本特性:封装,继承,多态。
封装
将一段逻辑/概念抽象出来做到“相对独立”。封装的概念并不是OOP独有的,而是长久以来一直被广泛采用的方法。主要目的总结为两点:
- 封装数据和实现细节。达到保护私有内容、使用者无需关心内部实现、且内部变化对使用者透明的目的。
- 封装变化。将不变和可变部分隔离,提升程序稳定性、复用性和可扩展性。
JavaScript中典型的封装就是模块化,实现方法有闭包、ES Module、AMD、CMD、CommonJS等。
多态
多态的概念也不是OOP独有的。所谓多态就是同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。
多态的目的就是用对象的多态性消除条件分支语句,提升程序的可拓展性。
JavaScript是一门弱类型多态语言,具有与生俱来的多态性。
继承
两大概念:
- 类(Class):抽象的模板;
- 实例(Instance):根据类创建的具体对象。
JavaScript中没有类的概念,使用构造函数作为对象模板,通过原型链来实现继承(ES6 中的Class
只是语法糖)。
原型及原型链相关知识详见深入JavaScript系列(六):原型与原型链
类式继承
将父类实例赋值给子类原型。缺点如下:
- 父类实例过早创建,无法接受子类的动态参数;
- 子类所有实例原型为同一父类实例,修改父类实例属性会影响所有子类实例。
function SupClass() {...}
function SubClass() {...}
SubClass.prototype = new SupClass()
构造函数式继承
子类构造函数中执行父类构造函数。缺点如下:
- 无法继承父类原型上的属性和方法。
function SupClass() {...}
function SubClass() {
SupClass.call(this, arguments)
}
组合式继承
类式继承+构造函数继承。缺点如下:
- 父类构造函数需调用两次。
function SupClass() {...}
function SubClass() {
SupClass.call(this, arguments)
}
SubClass.prototype = new SupClass()
原型式继承
对类式继承的封装,功能类似Object.create
,缺点如下:
- 若每次传入同一个原型,还是存在修改后影响其他子类实例的问题。
function createObj(o) {
function F() {}
F.prototype = o
return new F()
}
寄生式继承
拓展原型式继承
创建的对象并返回。
function createObj(o) {
const obj = Object.create(o)
obj.name = 'Logan'
return obj
}
寄生组合式继承
寄生式继承+构造函数式继承。
function inherit(child, parent) {
const p = Object.create(parent.prototype)
child.prototype = p
p.constructor = child
return child
}
function SupClass() {...}
function SubClass() {
SupClass.call(this)
}
SubClass = inherit(SubClass, SupClass)
开源项目中应用原型继承的案例
jQuery
var jQuery = function(selector, context) {
return new jQuery.fn.init(selector, context)
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
... // 各种原型方法
}
jQuery.fn.init = function(selector, context, root) { ... }
jQuery.fn.init.prototype = jQuery.fn // 校正实例的原型
Vue
function Vue(options) {
this._init(options)
}
// initMixin
Vue.prototype._init = function (options) { ... }
// stateMixin
Object.defineProperty(Vue.prototype, '$data', {...})
Object.defineProperty(Vue.prototype, '$props', {...})
Vue.prototype.$set = function() {...}
Vue.prototype.$delete = function() {...}
Vue.prototype.$watch = function() {...}
// eventMixin
Vue.prototype.$on = function() {...}
Vue.prototype.$once = function() {...}
Vue.prototype.$off = function() {...}
Vue.prototype.$emit = function() {...}
// lifecycleMixin
Vue.prototype._update = function() {...}
Vue.prototype.$forceUpdate = function() {...}
Vue.prototype.$destory = function() {...}
// renderMixin
Vue.prototype.$nextTick = function() {...}
Vue.prototype._render = function() {...}
new的详细过程及其模拟实现
new
一个对象的详细过程
- 创建一个全新对象,并将该对象原型指向构造函数的原型对象;
- 将构造函数调用的this指向这个新对象,并执行构造函数;
- 如果构造函数执行结果为对象类型(包含Object,Functoin, Array, Date, RegExg, Error等),则返回执行结果,否则返回创建的新对象。
模拟实现new
function newOperator(Ctor, ...args) {
if (typeof Ctor !== 'function') {
throw new TypeError('First argument is not a constructor')
}
// 1. 创建一个全新对象,并将该对象原型指向构造函数的原型对象
const obj = Object.create(Ctor.prototype)
// 2. 将构造函数调用的this指向这个新对象,并执行构造函数;
const result = Ctor.apply(obj, args)
// 3. 如果构造函数执行结果为对象类型,则返回执行结果,否则返回创建的新对象
return (result instanceof Object) ? result : obj
}
ES6 Class的底层实现原理
ES6 中的类
Class
,仅仅只是基于现有的原型继承的一种语法糖,我们一起来看一下Class
的底层实现原理。
Class的底层实现要素
- 只能使用new操作符调用
Class
; Class
可定义实例属性方法和静态属性方法;- 子
Class
的实例可继承父Class
上的实例属性方法、子Class
可继承父Class
上的静态属性方法。
只能使用new操作符调用Class
实现思路:使用instanceof
操作符检测实例是否为指定类的实例。
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
定义实例属性方法和静态属性方法
实现思路
- 在构造函数的原型上定义属性方法,即为实例属性方法;
- 在构造函数本身定义属性方法,即为静态属性方法。
function _createClass(Constructor, protoProps = [], staticProps = []) {
// 在构造函数的原型上定义实例属性方法
_defineProperties(Constructor.prototype, protoProps)
// 在构造函数本身定义静态属性方法
_defineProperties(Constructor, staticProps)
}
// 实现公用的批量给对象添加属性方法的方法
function _defineProperties(target, props) {
props.forEach(prop => {
Object.defineProperty(target, prop.key, prop)
})
}
继承实例属性方法和静态属性方法
实现思路:借用原型链继承实现。
function _inherits(subClass, superClass) {
// 子类实例继承父类的实例属性方法
subClass.prototype = Object.create(superClass.prototype)
// 修正constructor属性
subClass.prototype.constructor = subClass
// 子类继承父类的静态属性方法
Object.setPrototypeOf(subClass, superClass)
}
模拟编译
了解了Class
的底层实现要素,我们就来将Class
模拟编译为使用原型继承实现的代码。
源代码
class Person {
constructor(options) {
this.name = options.name
this.age = options.age
}
eat() {
return 'eating'
}
static isPerson(instance) {
return instance instanceof Person
}
}
class Student extends Person {
constructor(options) {
super(options)
this.grade = options.grade
}
study() {
return 'studying'
}
static isStudent(instance) {
return instance instanceof Student
}
}
编译后代码
var Person = (function() {
function Person(options) {
// 确保使用new调用
_classCallCheck(this, Person)
this.name = options.name
this.age = options.age
}
_createClass(
Person,
// 实例属性方法
[{
key: 'eat',
value: function eat() {
return 'eating'
}
}],
// 静态属性方法
[{
key: 'isPerson',
value: function isPerson(instance) {
return instance instanceof Person
}
}]
)
return Person
})();
var Student = (function (_Person) {
// 继承父类实例属性方法和静态属性方法
_inherits(Student, _Person)
function Student(options) {
// 确保使用new调用
_classCallCheck(this, Student)
// 执行父类构造函数
_Person.call(this, options)
this.grade = options.grade
}
_createClass(Student,
// 实例属性方法
[{
key: 'study',
value: function study() {
return 'studying'
}
}],
// 静态属性方法
[{
key: 'isStudent',
value: function isStudent(instance) {
return instance instanceof Student
}
}]
)
return Student
})(Person);
测试代码
const person = new Person({ name: 'Logan', age: 18 })
const student = new Student({ name: 'Logan', age: 18, grade: 9 })
expect(person.eat()).toBe('eating')
expect(student.eat()).toBe('eating') // 继承实例方法
expect(student.study()).toBe('studying')
expect(Student.isStudent(student)).toBe(true)
expect(Person.isPerson(person)).toBe(true)
expect(Student.isStudent(person)).toBe(false)
expect(Student.isPerson(student)).toBe(true) // 继承静态方法
公有字段和私有字段(提案中)
公有(public)和私有(private)字段声明目前在JavaScript标准委员会TC39的 试验性功能 (第3阶段),下面进行模拟实现。
静态公有字段
使用
class ClassWithStaticField {
static staticField1 = 'static field' // 设定初始值
static staticField2 // 不设定初始值
}
polyfill
function ClassWithStaticField() {}
// @babel/plugin-proposal-class-properties 中使用Object.defineProperty实现,此处简便起见直接赋值
ClassWithStaticField.staticField1 = 'static field' // 设定初始值
ClassWithStaticField.staticField2 = undefined // 不设定初始值
公有实例字段
使用
class ClassWithInstanceField {
instanceField1 = 'instance field' // 设定初始值
instanceField2 // 不设定初始值
}
polyfill
function ClassWithInstanceField() {
// @babel/plugin-proposal-class-properties 中使用Object.defineProperty实现,此处简便起见直接赋值
this.instanceField1 = 'instance field' // 设定初始值
this.instanceField2 = undefined // 不设定初始值
}
静态私有字段
静态私有字段只能在静态方法内访问,且只能通过类的属性进行访问,不能通过this进行访问。
使用
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD
static publicStaticMethod() {
ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD = 42
return ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD
}
}
polyfill
通过闭包实现静态私有字段
var ClassWithPrivateStaticField = (function() {
var _PRIVATE_STATIC_FIELD
function ClassWithPrivateStaticField() {}
ClassWithPrivateStaticField.publicStaticMethod = function() {
_PRIVATE_STATIC_FIELD = 42
return _PRIVATE_STATIC_FIELD
}
return ClassWithPrivateStaticField
})();
私有实例字段
使用
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
console.log(this.$privateField)
}
}
polyfill
通过WeakMap结合实例本身为key实现
var ClassWithPrivateField = (function() {
var _privateField = new WeakMap()
function ClassWithPrivateField() {
_privateField.set(this, undefined)
_privateField.set(this, 42)
console.log(_privateField.get(this))
}
})();