手写系列的代码,较为重要/内容较多的,都抽取到单独篇章去了,下面看大杂烩,概率会出的手写题目。
一 目录
不折腾的前端,和咸鱼有什么区别
二 自定义事件
返回目录
面试官:手写一个自定义原生事件。
简单三步曲:
- 创建自定义事件:
const myEvent = new Event('jsliangEvent')
- 监听自定义事件:
document.addEventListener(jsliangEvent)
- 触发自定义事件:
document.dispatchEvent(jsliangEvent)
简单实现:
window.onload = function() {
const myEvent = new Event('jsliangEvent');
document.addEventListener('jsliangEvent', function(e) {
console.log(e);
})
setTimeout(() => {
document.dispatchEvent(myEvent);
}, 2000);
};
页面 2 秒后自动触发 myEvent
事件。
2.1 创建自定义事件
返回目录
创建自定义事件的 3 种方法:
- 使用
Event
let myEvent = new Event('event_name');
- 使用
customEvent
(可以传参数)
let myEvent = new CustomEvent('event_name', {
detail: {
// 将需要传递的参数放到这里
// 可以在监听的回调函数中获取到:event.detail
}
});
- 使用
document.createEvent('CustomEvent')
和initEvent()
// createEvent:创建一个事件
let myEvent = document.createEvent('CustomEvent'); // 注意这里是 CustomEvent
// initEvent:初始化一个事件
myEvent.initEvent(
// 1. event_name:事件名称
// 2. canBubble:是否冒泡
// 3. cancelable:是否可以取消默认行为
)
2.2 事件的监听
返回目录
自定义事件的监听其实和普通事件一样,通过 addEventListener
来监听:
button.addEventListener('event_name', function(e) {})
2.3 事件的触发
返回目录
触发自定义事件使用 dispatchEvent(myEvent)
。
注意,这里的参数是要自定义事件的对象(也就是 myEvent
),而不是自定义事件的名称(myEvent
)
2.4 案例
返回目录
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>自定义事件</title>
</head>
<body>
<button class="btn">点我</button>
<script>
window.onload = function() {
// 方法 1
const myEvent = new Event('myEvent');
// 方法 2
// const myEvent = new CustomEvent('myEvent', {
// detail: {
// name: 'jsliang',
// },
// });
// 方法 3
// const myEvent = document.createEvent('CustomEvent');
// myEvent.initEvent('myEvent', true, true);
const btn = document.querySelector('.btn');
btn.addEventListener('myEvent', function(e) {
console.log(e);
})
setTimeout(() => {
btn.dispatchEvent(myEvent);
}, 2000);
};
</script>
</body>
</html>
上面 console.log(e)
输出:
/*
CustomEvent {
bubbles: true
cancelBubble: false
cancelable: true
composed: false
currentTarget: null
defaultPrevented: false
detail: null
eventPhase: 0
isTrusted: false
path: (5) [button.btn, body, html, document, Window]
returnValue: true
srcElement: button.btn
target: button.btn
timeStamp: 16.354999970644712
type: "myEvent"
}
*/
三 Object.create()
返回目录
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
。
function create(proto) {
function F() {};
F.prototype = proto;
return new F();
}
试验一下:
function create(proto) {
function F() {};
F.prototype = proto;
return new F();
}
const Father = function() {
this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
console.log(`我是你${this.bigName}`);
}
const Child = function() {
Father.call(this);
this.smallName = '儿子';
}
Child.prototype = create(Father.prototype);
Child.prototype.constructor = Child;
const child = new Child();
child.sayHello(); // 我是你爸爸
下面讲寄生组合式继承会用到 Object.create()
。
四 ES5 实现类继承
返回目录
使用 ES5 实现继承,简要在 3 行代码:
Father.call(this)
。在Child
中通过Father.call(this)
,将Father
的this
修改为Child
的this
Child.prototype = Object.create(Father.prototype)
。将Child
的原型链绑定到Father
的原型链上。Child.prototype.constructor = Child
。这个构造函数的实例的构造方法constructor
指向自身。
const Father = function (name, like) {
this.name = name;
this.like = like;
this.money = 10000000;
};
Father.prototype.company = function() {
console.log(`${this.name} 有 ${this.money} 元`);
}
const Children = function (name, like) {
Father.call(this);
this.name = name;
this.like = like;
}
Children.prototype = Object.create(Father.prototype);
Children.prototype.constructor = Children;
const jsliang = new Children('jsliang', '学习');
console.log(jsliang); // Children {name: "jsliang", like: "学习", money: 10000000}
jsliang.company(); // jsliang 有 10000000 元
需要注意 Child.prototype = Object.create(Father.prototype)
这句话:
- 这一步不用
Child.prototype = Father.prototype
的原因是怕共享内存,修改父类原型对象就会影响子类 - 不用
Child.prototype = new Parent()
的原因是会调用 2 次父类的构造方法(另一次是call
),会存在一份多余的父类实例属性 Object.create
是创建了父类原型的副本,与父类原型完全隔离
最后,这种继承方法,叫做 寄生组合式继承。
五 instanceof
返回目录
面试官:手写一个 instanceof
。
其实 instanceof
就是查找原型链的过程,如果你不懂原型和原型链,去看 jsliang 的原型和原型链文章先吧:
OK,那么有下面代码:
const Father = function() {
this.bigName = '爸爸';
};
Father.prototype.sayHello = function() {
console.log(`我是你${this.bigName}`);
}
const Child = function() {
Father.call(this);
this.smallName = '儿子';
}
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child;
const child = new Child();
child.sayHello(); // 我是你爸爸
console.log(child instanceof Child); // true
console.log(child instanceof Father); // true
console.log(child instanceof Object); // true
如何改造当中的 instanceof
呢?
function instanceOf(a, b) {
let proto = a.__proto__;
const prototype = b.prototype;
// 从当前 __proto__ 开始查找
while (proto) {
// 如果找到 null 还没有找到,返回 false
if (proto === null) {
return false;
}
// 如果 a.__proto__.xxx === b.prototype,返回 true
if (proto === prototype) {
return true;
}
// 进一步迭代
proto = proto.__proto__;
}
}
console.log(instanceOf(child, Child)); // true
console.log(instanceOf(child, Father)); // true
console.log(instanceOf(child, Object)); // true
输出结果同 instanceof
一样,完成目标!
才怪!!!
经过测试:
let num = 123;
console.log(num instanceof Object); // false
console.log(instancOf(123, Object)); // true
为什么呢?因为 instanceof
在原生代码上,实际是做了基本类型的检测,基本类型应该返回 false
,所以可以进行改造:
function instanceOf(a, b) {
// 新增:通过 typeof 判断基本类型
if (typeof a !== 'object' || b === null) {
return false;
}
// 新增:getPrototypeOf 是 Object 自带的一个方法
// 可以拿到参数的原型对象
let proto = Object.getPrototypeOf(a);
const prototype = b.prototype;
while (proto) {
if (proto === null) {
return false;
}
if (proto === prototype) {
return true;
}
proto = proto.__proto__;
}
}
六 柯里化
返回目录
实现一个 add
方法,使计算结果能够满足以下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
实现方法:
function add () {
const numberList = Array.from(arguments);
// 进一步收集剩余参数
const calculate = function() {
numberList.push(...arguments);
return calculate;
}
// 利用 toString 隐式转换,最后执行时进行转换
calculate.toString = function() {
return numberList.reduce((a, b) => a + b, 0);
}
return calculate;
}
// 实现一个 add 方法,使计算结果能够满足以下预期
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2, 3)(4)); // 10;
console.log(add(1)(2)(3)(4)(5)); // 15;
详细看 JavaScript 系列的闭包篇章,里面有讲解到闭包和柯里化。
七 迭代器
返回目录
迭代器的意思是:我的版本是可控的,你踢我一下,我动一下。
// 在数据获取的时候没有选择深拷贝内容
// 对于引用类型进行处理会有问题
// 这里只是演示简化了一点
function Iterdtor(arr) {
let data = [];
if (!Array.isArray(arr)) {
data = [arr];
} else {
data = arr;
}
let length = data.length;
let index = 0;
// 迭代器的核心 next
// 当调用 next 的时候会开始输出内部对象的下一项
this.next = function () {
let result = {};
result.value = data[index];
result.done = index === length - 1 ? true : false;
if (index !== length) {
index++;
return result;
}
// 当内容已经没有了的时候返回一个字符串提示
return 'data is all done';
};
}
const arr = [1, 2, 3];
// 生成一个迭代器对象
const iterdtor = new Iterdtor(arr);
console.log(iterdtor.next()); // { value: 1, done: false }
console.log(iterdtor.next()); // { value: 2, done: false }
console.log(iterdtor.next()); // { value: 2, done: true }
console.log(iterdtor.next()); // data is all done
八 Ajax
返回目录
通过 Promise
实现 ajax
:
index.json
{
"name": "jsliang",
"age": 25
}
index.js
const getData = (url) => {
return new Promise((resolve, reject) => {
// 设置 XMLHttpRequest 请求
const xhr = new XMLHttpRequest();
// 设置请求方法和 url
xhr.open('GET', url);
// 设置请求头
xhr.setRequestHeader('Accept', 'application/json');
// 设置请求的时候,readyState 属性变化的一个监控
xhr.onreadystatechange = (res) => {
// 如果请求的 readyState 不为 4,说明还没请求完毕
if (xhr.readyState !== 4) {
return;
}
// 如果请求成功(200),那么 resolve 它,否则 reject 它
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
};
// 发送请求
xhr.send();
})
};
getData('./index.json').then((res) => {
console.log(res); // { "name": "jsliang", "age": 25 }
})
补充:Ajax 状态
- 0 - 未初始化。尚未调用
open()
方法 - 1 - 启动。已经调用
open()
方法,但尚未调用send()
方法。 - 2 - 发送。已经调用
send()
方法,但尚未接收到响应。 - 3 - 接收。已经接收到部分响应数据。
- 4 - 完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
九 数组扁平化
返回目录
9.1 方法一:手撕递归
返回目录
const jsliangFlat = (arr) => {
// 1. 设置空数组
const result = [];
// 2. 设置递归
const recursion = (tempArr) => {
// 2.1 遍历数组
for (let i = 0; i < tempArr.length; i++) {
// 2.2 如果数组里面还是一个数组,那么递归它
if (Array.isArray(tempArr[i])) {
recursion(tempArr[i]);
} else { // 2.3 否则添加它
result.push(tempArr[i]);
}
}
};
recursion(arr);
// 3. 返回结果
return result;
};
console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]
9.2 方法二:flat()
返回目录
flat
方法可以扁平数组,如果不传参数,flat()
扁平一层,flat(2)
扁平 2 层,到 flat(Infinity)
扁平所有层。
const jsliangFlat = (arr) => {
return arr.flat(Infinity);
};
console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]
注意这个方法在 Node
执行会报错,这是一个 ES6 的方法。
9.3 方法三:reduce
返回目录
不推荐 reduce
,我怕小伙伴看得头晕。
const jsliangFlat = (arr = []) => {
return arr.reduce((prev, next) => {
if (Array.isArray(next)) {
return prev.concat(jsliangFlat(next));
} else {
return prev.concat(next);
}
}, [])
};
console.log(jsliangFlat([1, [2, [3, [4, [5]]]]])); // [1, 2, 3, 4, 5]
十 对象扁平化
返回目录
其实我也不知道这个有没考,也不是很难:
const obj = {
a: {
b: {
c: 1,
d: 2,
},
e: 3,
},
f: {
g: 4,
h: {
i: 5,
},
},
};
// 1. 设置结果集
const result = [];
// 2. 递归
const recursion = (obj, path = []) => {
// 2.1 如果到底部,此时 obj 是对应的值
if (typeof obj !== 'object') {
// 2.1.1 结果集加上这个字段
result.push({
[path.join('.')]: obj,
})
// 2.1.2 终止递归
return;
}
// 2.2 遍历 obj 对象
for (let i in obj) {
// 2.2.1 判断对象自身是否含有该字段(排除原型链)
if (obj.hasOwnProperty(i)) {
// 2.2.2 回溯,添加路径
path.push(i);
// 2.2.3 进一步递归
recursion(obj[i], path);
// 2.2.4 回溯,删除路径,方便下一次使用
path.pop();
}
}
};
recursion(obj);
// 3. 返回结果
console.log(result);
/*
[
{ 'a.b.c': 1 },
{ 'a.b.d': 2 },
{ 'a.e': 3 },
{ 'f.g': 4 },
{ 'f.h.i': 5 },
]
*/
还有反向推题:
- 根据
obj
和路径path
(a.b.c
),找到它的值
递归一下就行了,或者迭代也可以,不难。
写不出来的小哥反省下,写不出来的小姐姐找我,教你啊~ /手动狗头防暴力
十一 数组去重
返回目录
看着本文标题是 3 种,实际上有 5 种。
11.1 方法一:手撕去重
返回目录
const jsliangSet = (arr) => {
// 设置结果
const result = [];
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 如果结果集不包含这个元素
// 这里也可以用 result.indexOf(arr[i]) === -1
// 或者 arr.lastIndexOf(arr[i]) === i
if (!result.includes(arr[i])) {
result.push(arr[i]);
}
}
// 返回结果
return result;
};
console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]
11.2 方法二:Set
返回目录
const jsliangSet = (arr) => {
return [...new Set(arr)];
};
console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]
11.3 方法三:filter
返回目录
同样,通过 filter
也可以,其实内核也是 lastIndexOf
和当前索引值的一个比对。
const jsliangSet = (arr) => {
return arr.filter((item, index) => {
return arr.lastIndexOf(item) === index;
})
};
console.log(jsliangSet([1, 1, 1, 2, 2])); // [1, 2]
十二 其他
返回目录
其他的还有:
- 发布订阅模式:Node 回调函数、Vue event bus
- 异步并发数限制
- 异步串行|异步并行
- 图片懒加载
- 滚动加载
- 数组 API 实现:
filter
、map
、forEach
、reduce
- 大数据渲染(渲染几万条数据不卡页面)
- JSON:
JSON.parse()
、JSON.stringify()
但是 jsliang 可能面试见得少,我就不贴出来了,如果小伙伴们发现某个手写代码出现的挺频繁的,欢迎评论留言吐槽,jsliang 抽空将其写出来。
十三 参考文献
返回目录
- 解锁多种JavaScript数组去重姿势【阅读建议:20min】
- 如何在 JavaScript 中更好地使用数组【阅读建议:10min】
- 7种方法实现数组去重【阅读建议:20min】
jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。