暂时性死区
下面这个写法,不会报错且合法。因为 function 有作用域提升
可以先调用函数,后写 function
var es5Class = new ES5Class('instanceProp')
function ES5Class(instanceProp) {
this.instanceProp = instanceProp
}
那如果换成类呢?
这时就会报错,因为类不像 function 和 var,有作用域提升
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 调用')
}
}
访问器
访问器是不可枚举,且在实例和原型都存在的
如下图,颜色很淡,代表不可枚举
所以可以写个方法模拟他
/**
* 定义访问器属性,不可枚举
* ### 访问器属性是实例属性和原型属性的组合
*
* @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)
这有什么问题呢?
- 原型上是父类,但是这个原型你是通过
new Person()得到的。他的 name 属性也会放上原型
- 你直接覆盖了原型,你看你的 Student.prototype.fn 方法都没了
- 你没有把 Student 的构造函数重新改回来
改进方法就是拿一个中间函数,给他的原型赋值
同时把原型覆盖,改为方法合并 Object.setPrototypeOf,这样就能保留方法
function inherit(Target, Father) {
/**
* 用一个新函数,设置原型指向父类
* 此时去 new 这个新函数,就不会在原型上有 额外的属性
*/
function F() { }
F.prototype = Father.prototype
Object.setPrototypeOf(Target.prototype, new F())
Target.prototype.constructor = Target
}
效果如下,没有多余的属性,方法也不会被覆盖,构造器也是正确的
完整代码
'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
})()