手写promise
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
最简实现Promise,支持异步链式调用(20行)
apply/call/bind 原理
call、apply
和 bind
是挂在 Function
对象上的三个方法,调用这三个方法的必须是一个函数。
func.call(thisArg, param1, param2, ...)
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)
- 在浏览器里,在全局范围内this 指向window对象;
- 在函数中,this永远指向最后调用他的那个对象;
- 构造函数中,this指向new出来的那个新的对象;
call、apply、bind
中的this被强绑定在指定的那个对象上;- 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
apply、call、bind
都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
let a = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, 'poe', '24')
getValue.apply(a, ['poe', '24'])
bind
和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind
实现柯里化
方法的应用场景
下面几种应用场景,你多加体会就可以发现它们的理念都是“借用”方法的思路。我们来看看都有哪些。
- 判断数据类型
用
Object.prototype.toString
来判断类型是最合适的,借用它我们几乎可以判断所有类型的数据
function getType(obj){
let type = typeof obj;
if (type !== "object") {
return type;
}
return Object.prototype.toString.call(obj).replace(/^$/, '$1');
}
- 类数组借用方法
类数组因为不是真正的数组,所有没有数组类型上自带的种种方法,所以我们就可以利用一些方法去借用数组的方法,比如借用数组的
push
方法,看下面的一段代码。
var arrayLike = {
0: 'java',
1: 'script',
length: 2
}
Array.prototype.push.call(arrayLike, 'jack', 'lily');
console.log(typeof arrayLike); // 'object'
console.log(arrayLike);
// {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}
用
call
的方法来借用Array 原型链上的 push
方法,可以实现一个类数组的 push
方法,给arrayLike
添加新的元素
- 获取数组的最大 / 最小值
我们可以用 apply 来实现数组中判断最大 / 最小值,
apply
直接传递数组作为调用方法的参数,也可以减少一步展开数组,可以直接使用Math.max、Math.min
来获取数组的最大值 / 最小值,请看下面这段代码。
let arr = [13, 6, 10, 11, 16];
const max = Math.max.apply(Math, arr);
const min = Math.min.apply(Math, arr);
console.log(max); // 16
console.log(min); // 6
实现一个 bind 函数
对于实现以下几个函数,可以从几个方面思考
- 不传入第一个参数,那么默认为
window
- 改变了
this
指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
实现一个 call 函数
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'pp', '24') => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'pp', '24') => a.fn('pp', '24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
实现一个 apply 函数
Function.prototype.myApply = function(context = window, ...args) {
// this-->func context--> obj args--> 传递过来的参数
// 在context上加一个唯一值不影响context上的属性
let key = Symbol('key')
context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的方法
// let args = [...arguments].slice(1) //第一个参数为obj所以删除,伪数组转为数组
let result = context[key](...args);
delete context[key]; // 不删除会导致context属性越来越多
return result;
}
// 使用
function f(a,b){
console.log(a,b)
console.log(this.name)
}
let obj={
name:'张三'
}
f.myApply(obj,[1,2]) //arguments[1]
深浅拷贝
浅拷贝的原理和实现
方法一:object.assign
object.assign
是 ES6 中 object
的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝
。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
object.assign 的示例代码如下:
let target = {};
let source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };
但是使用 object.assign 方法有几点需要注意
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝
Symbol
类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);
从上面的样例代码中可以看到,利用 object.assign
也可以拷贝 Symbol
类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题
,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
方法二:扩展运算符方式
- 我们也可以利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能。
- 扩展运算符的语法为:
let cloneObj = { ...obj };
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和 object.assign
有同样的缺陷,也就是实现的浅拷贝的功能差不多
,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
方法三:concat 拷贝数组
数组的 concat
方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat
只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
方法四:slice 拷贝数组
slice
方法也比较有局限性,因为它仅仅针对数组类型
。slice方法会返回一个新的数组对象
,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
手工实现一个浅拷贝
根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:
- 对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝
。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下
:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
方法一:乞丐版(JSON.stringify)
JSON.stringify()
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON
的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse()
的方法将 JSON
字符串生成一个新的对象
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该方法也是有局限性的:
- 会忽略
undefined
- 会忽略
symbol
- 不能序列化函数
- 无法拷贝不可枚举的属性
- 无法拷贝对象的原型链
- 拷贝
RegExp
引用类型会变成空对象 - 拷贝
Date
引用类型会变成字符串 - 对象中含有
NaN
、Infinity
以及-Infinity
,JSON
序列化的结果会变成null
- 不能解决循环引用的对象,即对象成环 (
obj[key] = obj
)。
function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
使用
JSON.stringify
方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify
暂时还是无法满足的,那么就需要下面的几种方法了
方法二:基础版(手写递归实现)
下面是一个实现 deepClone 函数封装的例子,通过 for in
遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify
一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及
Symbol
类型; - 这种方法
只是针对普通的引用类型的值做递归复制
,而对于Array、Date、RegExp、Error、Function
这样的引用类型并不能正确地拷贝; - 对象的属性里面成环,即
循环引用没有解决
。
方法三:改进版(改进后递归实现)
针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。
- 针对能够遍历对象的不可枚举属性以及
Symbol
类型,我们可以使用Reflect.ownKeys
方法; - 当参数为
Date、RegExp
类型,则直接生成一个新的实例返回; - 利用
Object
的getOwnPropertyDescriptors
方法可以获得对象的所有属性,以及对应的特性,顺便结合Object.create
方法创建一个新对象,并继承传入原对象的原型链; - 利用
WeakMap
类型作为Hash
表,因为WeakMap
是弱引用类型,可以有效防止内存泄漏(你可以关注一下Map
和weakMap
的关键区别,这里要用weakMap
),作为检测循环引用很有帮助,如果存在循环,则引用直接返回WeakMap
存储的值
如果你在考虑到循环引用的问题之后,还能用 WeakMap
来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了
实现深拷贝
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) {
return new Date(obj) // 日期对象直接返回一个新的日期对象
}
if (obj.constructor === RegExp){
return new RegExp(obj) //正则对象直接返回一个新的正则对象
}
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) {
return hash.get(obj)
}
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 把cloneObj原型复制到obj上
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
我们看一下结果,cloneObj
在 obj
的基础上进行了一次深拷贝,cloneObj
里的 arr
数组进行了修改,并未影响到 obj.arr
的变化,如下图所示
节流与防抖
- 函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
// 函数防抖的实现
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
- 函数节流 是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
// 函数节流的实现;
function throttle(fn, delay) {
var preTime = Date.now();
return function() {
var context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}
手写观察者模式
当对象间存在一对多的关系,使用观察者模式。比如:当一个对象被修改,会自动通知依赖它的对象。
let uid = 0
class Dep {
constructor() {
this.id = uid++
// 存储所有的 watcher
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
if(this.subs.length) {
const index = this.subs.indexOf(sub)
if(index > -1) return this.subs.splice(index, 1)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
class Watcher {
constructor(name) {
this.name = name
}
update() {
console.log('更新')
}
}
手写发布订阅模式
与观察者模式相似,区别在于发布者和订阅者是解耦的,由中间的调度中心去与发布者和订阅者通信。
Vue响应式原理个人更倾向于发布订阅模式。其中 Observer 是发布者,Watcher 是订阅者,Dep 是调度中心。
class EventEmitter {
constructor() {
this.events = {}
}
on(type, cb) {
if(!this.events[type]) this.events[type] = []
this.events[type].push(cb)
}
emit(type, ...args) {
if(this.events[type]) {
this.events[type].forEach(cb => {
cb(...args)
})
}
}
off(type, cb) {
if(this.events[type]) {
const index = this.events[type].indexOf(cb)
if(index > -1) this.events[type].splice(index, 1)
}
}
}
手写ES5数组常见方法
实现forEach方法
Array.prototype.myForEach = function(callback, context=window) {
// this=>arr
let self = this,
i = 0,
len = self.length;
for(;i<len;i++) {
typeof callback == 'function' && callback.call(context,self[i], i)
}
}
实现filter方法
Array.prototype.myFilter=function(callback, context=window){
let len = this.length
newArr = [],
i=0
for(; i < len; i++){
if(callback.apply(context, [this[i], i , this])){
newArr.push(this[i]);
}
}
return newArr;
}
实现map方法
- 回调函数的参数有哪些,返回值如何处理
- 不修改原来的数组
Array.prototype.myMap = function(callback, context){
// 转换类数组
var arr = Array.prototype.slice.call(this),//由于是ES5所以就不用...展开符了
mappedArr = [],
i = 0;
for (; i < arr.length; i++ ){
// 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr))
mappedArr.push(callback.call(context, arr[i], i, this));
}
return mappedArr;
}
实现reduce方法
- 初始值不传怎么处理
- 回调函数的参数有哪些,返回值如何处理。
Array.prototype.myReduce = function(fn, initialValue) {
var arr = Array.prototype.slice.call(this);
var res, startIndex;
res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项
startIndex = initialValue ? 0 : 1;
for(var i = startIndex; i < arr.length; i++) {
// 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr))
res = fn.call(null, res, arr[i], i, this);
}
return res;
}
Array.isArray 实现
Array.myIsArray = function(o) {
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
console.log(Array.myIsArray([])); // true
Array.of 实现
Array.of()
方法用于将一组值,转换为数组
- 这个方法的主要目的,是弥补数组构造函数
Array()
的不足。因为参数个数的不同,会导致Array()
的行为有差异。 Array.of()
基本上可以用来替代Array()
或new Array()
,并且不存在由于参数不同而导致的重载。它的行为非常统一
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
实现
function ArrayOf(){
return [].slice.call(arguments);
}
find实现
find
接收一个方法作为参数,方法内部返回一个条件find
会遍历所有的元素,执行你给定的带有条件返回值的函数- 符合该条件的元素会作为 find 方法的返回值
- 如果遍历结束还没有符合该条件的元素,则返回
undefined
var users = [
{id: 1, name: '张三'},
{id: 2, name: '张三'},
{id: 3, name: '张三'},
{id: 4, name: '张三'}
]
Array.prototype.myFind = function (callback) {
// var callback = function (item, index) { return item.id === 4 }
for (var i = 0; i < this.length; i++) {
if (callback(this[i], i)) {
return this[i]
}
}
}
var ret = users.myFind(function (item, index) {
return item.id === 2
})
console.log(ret)
findIndex实现
var users = [
{id: 1, name: '张三'},
{id: 2, name: '张三'},
{id: 3, name: '张三'},
{id: 4, name: '张三'}
]
Array.prototype.myFindIndex = function (callback) {
// var callback = function (item, index) { return item.id === 4 }
for (var i = 0; i < this.length; i++) {
if (callback(this[i], i)) {
// 这里返回
return i
}
}
}
var ret = users.myFind(function (item, index) {
return item.id === 2
})
console.log(ret)
every方法
Array.prototype.myEvery=function(callback, context = window){
var len=this.length,
flag=true,
i = 0;
for(;i < len; i++){
if(!callback.apply(context,[this[i], i , this])){
flag=false;
break;
}
}
return flag;
}
// var obj = {num: 1}
// var aa=arr.myEvery(function(v,index,arr){
// return v.num>=12;
// },obj)
// console.log(aa)
some方法
Array.prototype.mySome=function(callback, context = window){
var len = this.length,
flag=false,
i = 0;
for(;i < len; i++){
if(callback.apply(context, [this[i], i , this])){
flag=true;
break;
}
}
return flag;
}
// var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)
// console.log(flag);
实现ES6的extends
function B(name){
this.name = name;
};
function A(name,age){
//1.将A的原型指向B
Object.setPrototypeOf(A,B);
//2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super
Object.getPrototypeOf(A).call(this, name)
//3.将A原有的属性添加到新实例上
this.age = age;
//4.返回新实例对象
return this;
};
var a = new A('poetry',22);
console.log(a);
对象扁平化
function objectFlat(obj = {}) {
const res = {}
function flat(item, preKey = '') {
Object.entries(item).forEach(([key, val]) => {
const newKey = preKey ? `${preKey}.${key}` : key
if (val && typeof val === 'object') {
flat(val, newKey)
} else {
res[newKey] = val
}
})
}
flat(obj)
return res
}
// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } }
console.log(objectFlat(source));
数组扁平化
需求:多维数组=>一维数组
let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);
调用ES6中的flat方法
ary = ary.flat(Infinity);
replace + split
ary = str.replace(/([|])/g, '').split(',')
replace + JSON.parse
str = str.replace(/([|])/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
普通递归
let result = [];
let fn = function(ary) {
for(let i = 0; i < ary.length; i++) {
let item = ary[i];
if (Array.isArray(ary[i])){
fn(item);
} else {
result.push(item);
}
}
}
利用reduce函数迭代
function flatten(ary) {
return ary.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))
扩展运算符
//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
ary = [].concat(...ary);
}
const 实现原理
由于ES5环境没有block
的概念,所以是无法百分百实现const
,只能是挂载到某个对象下,要么是全局的windo
w,要么就是自定义一个object
来当容器
var __const = function __const (data, value) {
window.data = value // 把要定义的data挂载到window下,并赋值value
Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
enumerable: false,
configurable: false,
get: function () {
return value
},
set: function (data) {
if (data !== value) { // 当要对当前属性进行赋值时,则抛出错误!
throw new TypeError('Assignment to constant variable.')
} else {
return value
}
}
})
}
__const('a', 10)
console.log(a)
delete a
console.log(a)
for (let item in window) { // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
if (item === 'a') { // 因为不可枚举,所以不执行
console.log(window[item])
}
}
a = 20 // 报错
手写深度比较isEqual
思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归
-
递归退出条件:
- 被比较的是两个值类型变量,直接用“===”判断
- 被比较的两个变量之一为null,直接判断另一个元素是否也为null
-
提前结束递推:
- 两个变量keys数量不同
- 传入的两个参数是同一个变量
-
递推工作: - 深度比较每一个key
function isEqual(obj1, obj2){
//其中一个为值类型或null
if(!isObject(obj1) || !isObject(obj2)){
return obj1 === obj2;
}
//判断是否两个参数是同一个变量
if(obj1 === obj2){
return true;
}
//判断keys数是否相等
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if(obj1Keys.length !== obj2Keys.length){
return false;
}
//深度比较每一个key
for(let key in obj1){
if(!isEqual(obj1[key], obj2[key])){
return false;
}
}
return true;
}
实现一个JSON.stringify
JSON.stringify(value[, replacer [, space]]):
Boolean | Number| String
类型会自动转换成对应的原始值。undefined
、任意函数以及symbol
,会被忽略(出现在非数组对象的属性值中时),或者被转换成null
(出现在数组中时)。- 不可枚举的属性会被忽略如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略
- 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略
function jsonStringify(obj) {
let type = typeof obj;
if (type !== "object") {
if (/string|undefined|function/.test(type)) {
obj = '"' + obj + '"';
}
return String(obj);
} else {
let json = []
let arr = Array.isArray(obj)
for (let k in obj) {
let v = obj[k];
let type = typeof v;
if (/string|undefined|function/.test(type)) {
v = '"' + v + '"';
} else if (type === "object") {
v = jsonStringify(v);
}
json.push((arr ? "" : '"' + k + '":') + String(v));
}
return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
}
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
实现一个JSON.parse
JSON.parse(text[, reviver])
用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换(操作)
第一种:直接调用 eval
function jsonParse(opt) {
return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
避免在不必要的情况下使用
eval
,eval()
是一个危险的函数,他执行的代码拥有着执行者的权利。如果你用eval()
运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。它会执行JS代码,有XSS漏洞。
如果你只想记这个方法,就得对参数json做校验。
var rx_one = /^[],:{}\s]*$/;
var rx_two = /\(?:["\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\n\r]*"|true|false|null|-?\d+(?:.\d*)?(?:[eE][+-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*[)+/g;
if (
rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
第二种:Function
核心:Function与eval有相同的字符串参数特性
var func = new Function(arg1, arg2, ..., functionBody);
在转换JSON的实际应用中,只需要这么做
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
eval
与Function
都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用
实现Event(event bus)
event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础
简单版:
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 储存事件/回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听上限
}
}
// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从储存事件键值对的this._events中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
return true;
};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将type事件以及对应的fn函数放入this._events中储存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};
面试版:
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 储存事件/回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听上限
}
}
// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从储存事件键值对的this._events中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
return true;
};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将type事件以及对应的fn函数放入this._events中储存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};
// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一个数组说明有多个监听者,需要依次此触发里面的函数
for (let i = 0; i < handler.length; i++) {
if (args.length > 0) {
handler[i].apply(this, args);
} else {
handler[i].call(this);
}
}
} else {
// 单个函数的情况我们直接触发即可
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
}
return true;
};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单
if (!handler) {
this._events.set(type, fn);
} else if (handler && typeof handler === "function") {
// 如果handler是函数说明只有一个监听者
this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存
} else {
handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可
}
};
EventEmeitter.prototype.removeListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单
// 如果是函数,说明只被监听了一次
if (handler && typeof handler === "function") {
this._events.delete(type, fn);
} else {
let postion;
// 如果handler是数组,说明被监听多次要找到对应的函数
for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
postion = i;
} else {
postion = -1;
}
}
// 如果找到匹配的函数,从数组中清除
if (postion !== -1) {
// 找到数组对应的位置,直接清除此回调
handler.splice(postion, 1);
// 如果清除后只有一个函数,那么取消数组,以函数形式保存
if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {
return this;
}
}
};
模拟Object.create
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 模拟 Object.create
function create(proto) {
function F() {}
F.prototype = proto;
return new F();
}
模板引擎实现
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template, data) {
const reg = /{{(\w+)}}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}