经典面试题,用ES5实现一个类,包含继承、私有属性、访问器、暂时性死区...

543 阅读5分钟

暂时性死区

下面这个写法,不会报错且合法。因为 function 有作用域提升

可以先调用函数,后写 function

var es5Class = new ES5Class('instanceProp')

function ES5Class(instanceProp) {
    this.instanceProp = instanceProp
}

那如果换成类呢?

这时就会报错,因为类不像 functionvar,有作用域提升

test.js:8 Uncaught
ReferenceError: Cannot access 'ES6Class' before initialization
var es6Class = new ES6Class('instanceProp')

class ES6Class {
    constructor(instanceProp) {
        this.instanceProp = instanceProp
    }
}

怎么解决呢?

你用 const、let 肯定不行,因为这是 ES6 的语法

那只能用 立即执行函数 模拟一下了

这样,你提前访问就只能拿到 undefined

var es5Class = new ES5Class('instanceProp')

var ES5Class = (function () {
    function ES5Class(instanceProp) {
        this.instanceProp = instanceProp
    }

    return ES5Class
})()

严格模式

类默认开启严格模式,所以你要声明一下 'use strict'

'use strict'

var ES5Class = (function () {
    function ES5Class(instanceProp) {
        this.instanceProp = instanceProp
    }
    
    return ES5Class
})()

私有属性

现在可以使用 #xxx 来开启私有属性,比如

class ES6Class {
    #__privateProp__ = '__privateProp__'
}

但是这个特性需要新版本才有,降级方案如下

Symbol

Symbol 是一个唯一的键,你不导出别人就没法用

但是还有后门呢,可以通过 Object.getOwnPropertySymbols 获取

而且 ES5 也用不了 Symbol

这个方案不行

'use strict'

var ES5Class = (function () {
    var privateKey = Symbol()

    function ES5Class(instanceProp) {
        this.instanceProp = instanceProp

        this[privateKey] = 'Symbol 私有属性'
    }
    
    return ES5Class
})()

var es5Class = new ES5Class('instanceProp')

// 还是能拿到 Symbol
var key = Object.getOwnPropertySymbols(es5Class)[0]
console.log(es5Class[key])

Map | WeakMap

可以用 Map 把属性存起来,this 为键,属性为值。你不导出别人就用不了

WeakMap 是弱引用,防止内存泄露的

var privateMap = new WeakMap()

/**
 * 定义私有属性
 * 
 * @param {*} instance 实例
 * @param {*} key 键
 * @param {*} value 值
 */
function definePrivateProp(instance, key, value) {
    let map = privateMap.get(instance)
    if (!map) {
        map = { [key]: value }
    }
    else {
        map[key] = value
    }

    privateMap.set(instance, map)
}

/**
 * 获取私有属性
 * 
 * @param {*} instance 实例
 * @param {*} key 键
 * @returns {*} 值
 */
function getPrivateProp(instance, key) {
    return privateMap.get(instance)[key]
}

使用方法如下

 function ES5Class() {
    definePrivateProp(this, 'privateProp', '__privateProp__')

    console.log(getPrivateProp(this, 'privateProp'))
}

不过 Map 也是 ES6 的东西

所以要实现的话,你也可以换成对象,键就用 类名 + 唯一标识

或者函数闭包,外面也不能访问

检查是否为 new 调用

可以通过 new.target 判断,如果是函数调用,这个值是 undefined

可惜他又是 ES6 的东西呢

降级方案可以通过原型比较

/**
 * - 如果通过 new 调用,this 的隐式原型指向类,返回 true
 * - 如果是函数调用,this 的隐式原型指向 window(非严格模式),返回 false
 * 
 * - new.target 是 ES6 的语法,用来判断是否通过 new 调用
 * - 下面这种方法兼容性好
 * 
 * @param {*} instance 实例
 * @param {*} Cls 类
 * @returns {boolean}
 */
function isUseNew(instance, Cls) {
    return Object.getPrototypeOf(instance) === Cls.prototype
}

用法如下

function ES5Class() {
    if (!isUseNew(this, ES5Class)) {
        throw new Error('必须通过 new 调用')
    }
}

访问器

访问器是不可枚举,且在实例和原型都存在的

如下图,颜色很淡,代表不可枚举

image.png

所以可以写个方法模拟他

/**
 * 定义访问器属性,不可枚举
 * ### 访问器属性是实例属性和原型属性的组合
 * 
 * @param {*} instance 实例
 * @param {*} key 键
 * @param {*} getter 访问器函数
 */
function defineClassGetter(instance, key, getter) {
    Object.defineProperty(instance, key, {
        get: getter,
        enumerable: false
    })

    /**
     * 访问器属性是实例属性和原型属性的组合
     */
    var prototype = Object.getPrototypeOf(instance)
    Object.defineProperty(prototype, key, {
        get: getter,
        enumerable: false
    })
}

方法

类的方法是在原型上

且不可枚举

不可通过 new 调用

那么又可以定义一个方法实现

注意:你要保存 this,因为包装了一层函数判断 new

/**
 * 定义方法,不可枚举,并检查是否通过 new 调用方法
 * 
 * @param {*} prototype 原型
 * @param {*} key 键
 * @param {*} fn 方法
 */
function defineClassMethod(prototype, key, fn) {
    Object.defineProperty(prototype, key, {
        value: function () {
            var self = this
            if (!isUseNew(this, ES5Class)) {
                throw new Error('不能通过 new 调用方法')
            }
            fn.apply(self, arguments)
        },
        enumerable: false
    })
}

继承

来看一段错误示范

/**
 * 父类
 */
function Person(name) {
    console.log('init Person')
    this.name = name
}
Person.prototype.sayHello = function () {
    console.log(this.name + ' say hello')
}


/**
 * 子类
 */
function Student(name, score) {
    Person.call(this, name)
    console.log('init Student')

    this.name = name
    this.score = score
}
Student.prototype.fn = function () {
    console.log(this.name + ' 分数:' + this.score + ' go school')
}

/**
 * 继承
 */
Student.prototype = new Person()


var s = new Student('name', 100)
console.log(s)

这有什么问题呢?

  1. 原型上是父类,但是这个原型你是通过 new Person() 得到的。他的 name 属性也会放上原型
image.png
  1. 你直接覆盖了原型,你看你的 Student.prototype.fn 方法都没了
  2. 你没有把 Student 的构造函数重新改回来

改进方法就是拿一个中间函数,给他的原型赋值

同时把原型覆盖,改为方法合并 Object.setPrototypeOf,这样就能保留方法

function inherit(Target, Father) {
    /**
     * 用一个新函数,设置原型指向父类
     * 此时去 new 这个新函数,就不会在原型上有 额外的属性
     */
    function F() { }
    F.prototype = Father.prototype

    Object.setPrototypeOf(Target.prototype, new F())
    Target.prototype.constructor = Target
}

效果如下,没有多余的属性,方法也不会被覆盖,构造器也是正确的

image.png

完整代码

'use strict'

/**
 * 立即执行函数是模拟 ES6 class 的暂时性死区,避免函数提升
 */
var ES5Class = (function () {
    var privateMap = new WeakMap()

    function ES5Class(instanceProp) {
        if (!isUseNew(this, ES5Class)) {
            throw new Error('必须通过 new 调用')
        }

        this.instanceProp = instanceProp

        defineClassGetter(this, 'getter', function () {
            return '访问器属性'
        })

        definePrivateProp(this, 'privateProp', '__privateProp__')
    }

    defineClassMethod(ES5Class.prototype, 'method', function () {
        console.log(`实例方法被调用,私有属性为:${getPrivateProp(this, 'privateProp')}`)
    })
    ES5Class.staticProp = '静态属性'


    /**
     * 定义私有属性
     * 
     * @param {*} instance 实例
     * @param {*} key 键
     * @param {*} value 值
     */
    function definePrivateProp(instance, key, value) {
        let map = privateMap.get(instance)
        if (!map) {
            map = { [key]: value }
        }
        else {
            map[key] = value
        }

        privateMap.set(instance, map)
    }

    /**
     * 获取私有属性
     * 
     * @param {*} instance 实例
     * @param {*} key 键
     * @returns {*} 值
     */
    function getPrivateProp(instance, key) {
        return privateMap.get(instance)[key]
    }

    /**
     * - 如果通过 new 调用,this 的隐式原型指向类,返回 true
     * - 如果是函数调用,this 的隐式原型指向 window(非严格模式),返回 false
     * 
     * - new.target 是 ES6 的语法,用来判断是否通过 new 调用
     * - 下面这种方法兼容性好
     * 
     * @param {*} instance 实例
     * @param {*} Cls 类
     * @returns {boolean}
     */
    function isUseNew(instance, Cls) {
        return Object.getPrototypeOf(instance) === Cls.prototype
    }

    /**
     * 定义访问器属性,不可枚举
     * ### 访问器属性是实例属性和原型属性的组合
     * 
     * @param {*} instance 实例
     * @param {*} key 键
     * @param {*} getter 访问器函数
     */
    function defineClassGetter(instance, key, getter) {
        Object.defineProperty(instance, key, {
            get: getter,
            enumerable: false
        })

        /**
         * 访问器属性是实例属性和原型属性的组合
         */
        var prototype = Object.getPrototypeOf(instance)
        Object.defineProperty(prototype, key, {
            get: getter,
            enumerable: false
        })
    }

    /**
     * 定义方法,不可枚举,并检查是否通过 new 调用方法
     * 
     * @param {*} prototype 原型
     * @param {*} key 键
     * @param {*} fn 方法
     */
    function defineClassMethod(prototype, key, fn) {
        Object.defineProperty(prototype, key, {
            value: function () {
                var self = this
                if (!isUseNew(this, ES5Class)) {
                    throw new Error('不能通过 new 调用方法')
                }
                fn.apply(self, arguments)
            },
            enumerable: false
        })
    }

    return ES5Class
})()