原型与原型链可以说是JavaScript中最重要的概念了, JavaScript是基于面向对象的思想设计的语言, 而前两者则是这种思想的集中体现, 理解了原型与原型链, 才能更好的了解JavaScript这门语言, 为此我决定系统地梳理一下.
构造函数设计模式
我们先来看看构造函数与普通函数执行的差异, 普通函数执行特性, 我在JavaScript数据类型细节点: 用简单的方式让你理解概念的文末做了详细的阐述, 不懂的同学可以看看哦!🤭
构造函数在创建与执行时,与普通函数的差异:
-
构造函数在初始化this指向的时候, 会使它指向一个空实例对象.
-
并且在构造函数内部, 所有关于
this.xx
的成员访问都是直接访问的实例对象内的成员. -
构造函数会默认返回实例对象, 如果我们自行添加了返回值则:
-
返回的如果是一个对象, 则覆盖实例对象
-
返回的如果是一个原始值, 则默认返回实例对象
-
proptotype
绝大多数函数都带有prototype原型属性, 这个属性是一个对象, 浏览器会默认开启一个堆内存, 用来存储当前类所属实例的公有属性与方法.
具备prototype属性的函数:
- 普通函数
- 构造函数/类(ES6)
- 生成器函数generator
不具备prototy属性的函数:
- 箭头函数
- 基于ES6对对象某个成员赋值函数的操作, 如:
function func(){
fun1: function(){} // 非简写
func2(){
// ... 简写, 不具备prototype
}
}
class{
constructor(){}
fn(){
// ... 简写, 不具备prototype
}
}
原型重定向
值得注意的一点是: 手写的类允许重定向, 但是内置类只允许添加, 不允许重定向, 不过可以针对单一函数进行逐一修改.
那如何在原型中批量添加方法?
-
Object,assign(obj1, obj2)
- assign会返回合并后的第一个对象, 所以obj1 要作为我们的目标对象
2, 利用闭包的形式批量添加
// 1.
Object.assign(Number.prototype, {
plus: function plus (num) {
return num++
}
})
// 2.
(function (prototype) {
const plus = function plus (num) {
return num++
}
prototype.plus = plus
})(Number.prototype)
如何检测一个属性是否为对象的公有/私有属性
原生给我们提供了一些办法, 但是还不大够用.
-
obj.hasOwnProperty([attr]): 该属性是否为对象的私有属性?
-
[attr] in obj: 是否为obj的属性
那公有属性看起来是似乎可以这么做:
function hasPubProperty (obj, attr) {
return (attr in obj) && !obj.hasOwnProperty(attr)
}
我相信你也很快看出了问题, 如果一个属性,它既是私有属性, 又是共有属性, 那返回的也是false, 所以我们来看看更好的写法.
解决办法:
-
Object.keys(obj): 遍历obj
-
Object.getOwnPropertyNames(obj): 获取非
Symbol
私有属性. -
Object.getOwnPropertySymbols(obj): 获取私有的
Symbol
属性
1. 获取私有属性
function each(obj, callback) {
let objKeys = Object.keys(obj),
len = 0,
i = 0
// 确定当前版本存在Symbol
if (typeof Symbol !== 'undefined') {
// 获取Symbol属性
objKeys = objKeys.concat(Object.getOwnPropertySymbols(obj))
}
len = objKeys.length
for (; i < len; i++) {
let key = objKeys[i],
value = obj[key]
callback(key, value)
}
}
each(obj, (key, value) => {
console.log(key, value)
})
2. 检测是否为公有属性
Object.prototype.hasPubProperty = function hasPubProperty(attr) {
let self = this,
prototype = Object.getPrototypeOf(attr)
while (prototype) {
if (prototype.hasOwnProperty(attr)) return true
prototype = Object.getPrototypeOf(prototype)
}
return false
}
__proto__(原型链)(⭐⭐)
每一个对象数据类型的值都具有一个__proto__(原型链), 属性值指向了自己所属的原型对象(prototype)
这个概念可以说是所有萌新的噩梦, 不过只要把图画出来, 其实也不过如此:
function A(){} // 构造函数
let a = new A()
let o = new Object()
针对上述两个实例, 我们来画一张图:
可能在顶级的函数与对象之间, 原型链会比较复杂, 但对此我们仍可以总结出一些规律:
- 所有实例的
__proto__
都会指向它构造函数的原型对象(prototype)
- 所有函数对象的
__proto__
, 都会指向顶级构造函数Function的原型对象(prototype)
- 对象没有原型, 原型是函数独有的一个属性.
基于上述几点, 我们可以通过图可以看出, 其实总体来说, 顶级函数
和顶级对象
, 他们都具有一个普通对象和函数该有的特质, 只是指向上有一些复杂.
new的工作原理(阿里面试题)
new执行构造函数, 会返回一个实例对象, 实例对象的原型链会指向构造函数的原型对象
new有哪些注意点
-
改变构造函数执行的this指向, 并且令函数返回这个实例
-
构造函数必须是一个函数
-
不能是
Symbol
或者Bigint
类型, 它们不能被new -
创造的实例必须指向构造函数的原型(prototype)
-
对于new执行函数而言, 默认情况下要返回一个实例对象, 如果我们改写了返回值, 如果是对象则返回, 如果是原始值则不改写返回值.
new的实现:(⭐)
function _new(Ctor, ...params) {
let prototype = Ctor.prototype,
CtorType = typeof Ctor,
self,
result
if (CtorType === 'symbol' || CtorType === 'bigint' || !prototype || CtorType !== 'function') {
throw new TypeError(`${Ctor} is not a constructor`)
}
// 创建一个空实例对象
self = create(prototype)
// Ctor执行但this指向实例
result = Ctor.apply(self, params)
// 判断返回值
if (result !== null && /^(object|function)$/.test(typeof result)) return result
return self
}
// 创造一个空对象实例指向参数
function create(prototype) {
if (prototype !== null && typeof prototype !== 'object') {
throw new TypeError('balabala')
}
function Proxy() {}
Proxy.prototype = prototype
return new Proxy()
}
ES6 类class
跟构造函数大同小异, 只是无法直接执行
注意点:
- 构造体内的参数直接挂载在实例对象上
- 无法在原型上设置非函数的公有属性
class Fn {
// 构造函数体, 直接挂在实例对象上
constructor(name) {
this.name = name
this.getName = function getName() {
//...
}
}
// 无法在原型上设置非函数的公有属性.
age = 12 // 等同于在构造体内 this.age = 12
getAge() {}
// static 关键字可以让方法挂在构造函数上(相当于一个普通函数)
static sex = 1
static getSex = function getSex() {}
}
Fn.prototype.getAllInfo = function getAllInfo() {
//..
}
this
注意点:
-
给事件绑定事件时, this会指向Dom对象本身 ( 排除IE8- )
-
箭头函数,块级上下文,没有this, 如果上级上下文有this, 他会继承上级上下文的this, 但无法改变指向
-
bind 改变允许函数异步执行.
实现 call (⭐)
call
与apply
并没有太大的区别, 仅是传入参数不同.
Function.prototype._call = function _call(context, ...params) {
// 1. 将context作为函数执行的this
// 2. 执行原来的函数
context == null ? (context = window) : context
// 原始值则装箱
if (!/^(function|object)$/.test(typeof context)) context = Object(context)
let self = this,
key = Symbol('key'),
result
// 防止与其他属性冲突
context[key] = self
result = context[key](...params)
delete context[key]
return result
}
实现 bind (⭐)
bind 与 call / apply 最大的区别在于, 允许如事件绑定这样的行为异步执行. 本质上就是返回的不是函数的执行结果, 将其结果包裹在一个代理函数下, 待到事件出发才执行.
有一点需要注意的是: 在bind函数
中, 由于我们返回的是一个代理函数, 那用户就有可能会给这个代理函数传参, 如在事件绑定中, 给函数传入事件对象event
, 所以我们需要取到代理函数的实参集合, 合并到我们的params
中.
(代理函数内部的做法, 其他部分就与_call
一样了, 请允许我偷个懒, 如要复制代码, 记得也把👆上面的_call方法也复制过去哦)
Function.prototype._bind = function _bind(context, ...params) {
let self = this
return function proxy() {
params = params.concat(...arguments)
return self._call(context, ...params)
}
}
感谢😘
如果觉得文章内容对你有帮助:
-
❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁