一文搞懂手写底层源码
1.手写 Promise.all()
Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入。 —— 说明所传参数都具有 Iterable,也就是可遍历。
并且只返回一个 Promise 实例。—— 说明最终返回是一个 Promise 对象。
那个输入的所有 promise 的 resolve 回调的结果是一个数组。—— 说明最终返回的结果是一个数组,且数组内数据要与传参数据对应。
这个 Promise 的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。—— 说明最终返回时,要包含所有的结果的返回。
它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。—— 说明只要一个报错,立马调用 reject 返回错误信息。
const PromiseAll = (iterator) => {
const promises = Array.from(iterator); // 对传入的数据进行浅拷贝,确保有遍历器(也可以不拷贝,都行)
const len = promises.length; // 长度
let index = 0; // 每次执行成功+1,当等于长度时,说明所有数据都返回,则可以resolve
let tempList = []; // 用来存放结果的数组
return new Promise((resolve, reject) => {
for (let i of promises) {
promises[i]
.then((res) => {
tempList[i] = res;
if (++index === len) {
//根据空数组(执行完毕的数组)length等于当前遍历索引来判断,即如果全都执行完毕了
resolve(data); //全部执行完毕了,再去resolve,返回的依然是Promise,一个成功对象!
}
})
.catch((err) => {
reject(err);
});
}
});
};
const promise1 = Promise.resolve("promise1");
const promise2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 2000, "promise2"); //resolve可以不加括号,'promise2'为其命名
});
const promise3 = new Promise(function (resolve, reject) {
setTimeout(resolve, 1000, "promise3");
});
PromiseAll([promise1, promise2, promise3]).then(function (values) {
console.log(values);
});
3.手写深拷贝
简单地进行深拷贝:
const s1 = Symbol();
const s2 = Symbol();
const obj = {
name: "why",
friend: {
name: "kobe",
},
foo: function () {
console.log("foo function");
},
[s1]: "abc",
s2: s2,
};
obj.inner = obj; //对象有个inner属性指向他自己,如果用了json转换之后就不可以了,会报错
const info = JSON.parse(JSON.stringify(obj)); //主要是这里实现了深拷贝,但是会有问题!因为json在转换的过程中不识别函数和symbol!
console.log(info === obj);
obj.friend.name = "james";
console.log(info);
手写实现:
function isObject(value) {
//const valueType = typeof value
//return (value !== null) && (valueType === "object" || valueType === "function")
return value instanceof Object;
}
function deepClone(originValue) {
// 判断传入的originValue是否是一个对象类型
if (!isObject(originValue)) {
return originValue; //如果不是对象,那说明是一个属性值,直接返回就可以了!后面的每一层属性值都是这里返回的!
}
const newObject = {}; //每次递归返回的一个新的拷贝值的对象
for (const key in originValue) {
//把每个属性都遍历进行重新赋值!使用了for in
//每一个都等于递归!
newObject[key] = deepClone(originValue[key]); //将属性值一一对应进行深拷贝,一层一层最终返回成一个完全一样的对象,拷贝旧对象的这个键值,给新对象!
}
return newObject; //第一层,以及后面的每一层对象,都是这里返回的!
}
// 测试代码
const obj = {
name: "why",
age: 18,
friend: {
name: "james",
address: {
city: "广州",
},
},
};
const newObj = deepClone(obj);
console.log(newObj === obj);
obj.friend.name = "kobe";
obj.friend.address.city = "成都";
console.log(newObj);
4.手写图片懒加载
<div class="container">
<img src="loading.gif" data-src="pic.png" />
<img src="loading.gif" data-src="pic.png" />
<img src="loading.gif" data-src="pic.png" />
<img src="loading.gif" data-src="pic.png" />
<img src="loading.gif" data-src="pic.png" />
<img src="loading.gif" data-src="pic.png" />
</div>
<script>
var imgs = document.querySelectorAll("img");
function lozyLoad() {
//document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动过的距离
//window.innerHeight 是浏览器可视区的高度
var scrollTop =
document.body.scrollTop || document.documentElement.scrollTop;
var winHeight = window.innerHeight;
for (var i = 0; i < imgs.length; i++) {
//imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
if (imgs[i].offsetTop < scrollTop + winHeight) {
//当图片进入视野时
imgs[i].src = imgs[i].getAttribute("data-src");
}
}
}
//在滚动的过程中调用函数
window.onscroll = lozyLoad();
</script>
5.手写防抖和节流
防抖
//接一个订单,和等待时间
function debounce(fn, delay) {
let timerId = null;
return function () {
const context = this;
//如果接到新订单,但是已经在等待中了,就重新开始计时,再等3分钟
if (timerId) {
window.clearTimeout(timerId);
}
//3分钟没有接到订单就直接配送
timerId = setTimeout(() => {
fn.apply(context, arguments);
timerId = null; //这个也是必要的
}, delay);
};
}
节流
function throttle(fn, delay) {
// 设置一个触发开关
let canUse = true;
return function () {
//都是return一个function,这个是一个异步的包含setTimeout的
//如果开关为true,就立即触发技能,否则就不能触发
if (canUse) {
fn.apply(this, arguments); //触发技能,也可以直接fn()应该
//触发技能后,直接关闭开关
canUse = false;
//在3秒后打开开关,这样新的请求才可能再请求成功!
setTimeout(() => (canUse = true), delay);
}
};
}
7.js 手写轮播图(实现原理)
<style type="text/css">
#counter {
width: 600px;
height: 300px;
position: relative;
overflow: hidden;
}
#list {
width: 3600px;
height: 300px;
position: absolute;
z-index: 1;
}
#list img {
width: 600px;
height: 300px;
float: left;
}
.arrow {
position: absolute;
top: 110px;
text-decoration: none;
z-index: 2;
display: none;
width: 40px;
height: 40px;
font-size: 36px;
font-weight: bold;
line-height: 39px;
text-align: center;
color: #fff;
background-color: rgba(0, 0, 0, 1);
cursor: pointer;
}
.arrow:hover {
background-color: rgba(0, 0, 0, 0.8);
}
#counter:hover .arrow {
display: block;
}
#pre {
left: 20px;
}
#next {
right: 20px;
}
#list {
transition: left 1s;
}
.list {
transition: left 0.1s;
}
#buttons {
position: absolute;
height: 10px;
width: 120px;
left: 250px;
bottom: 20px;
z-index: 2;
}
#buttons span {
border: 1px solid #ffffff;
border-radius: 5px;
float: left;
width: 10px;
height: 10px;
background-color: #333;
margin-right: 5px;
cursor: pointer;
}
#buttons .on {
background: orangered;
}
</style>
<div id="counter">
<div id="list" style="left:0px;">
<!-- 设置初始偏移量为0px -->
<img src="img/img1.jpg" alt="1" />
<img src="img/img2.jpg" alt="2" />
<img src="img/img3.jpg" alt="3" />
<img src="img/img4.jpg" alt="4" />
<img src="img/img5.jpg" alt="5" />
<img src="img/img6.jpg" alt="6" />
</div>
<!-- 图片两边的左右点击切换图片按钮 -->
<a href="javascript:;" id="pre" class="arrow"><</a>
<a href="javascript:;" id="next" class="arrow">></a>
</div>
<script type="text/javascript">
var counter = document.getElementById("counter");
var list = document.getElementById("list");
var pre = document.getElementById("pre");
var next = document.getElementById("next");
var timer;
//html与js结合式书写 : 变量名.style.left=数值
var nextlist = parseInt(list.style.left); //接收偏移量的值
// var index=1;
// var buttons=document.getElementById("buttons").getElementById('span');
//偏移量的改变
function animals(offset) {
var newlist = parseInt(list.style.left) + offset; //定义参数随时传递新的偏移量值
list.style.left = newlist + "px"; //偏移量需要单位‘像素px’,否则计算机识别不出,图片将不会移动位置
//到达最后一张时,点击右耳朵则返回到第一张
if (newlist < -3001) {
list.style.left = 0 + "px";
list.setAttribute("class", "list");
}
//在第一张时,点左耳朵则返回到最后一张
if (newlist > 0) {
list.style.left = -3000 + "px";
list.setAttribute("class", "list");
}
}
// 点击左右耳朵触发函数
pre.onclick = function () {
animals(600); //点击左边耳朵,图片往左移一张,偏移量加600
};
next.onclick = function () {
animals(-600); //点击右边耳朵,图片往右移一张,偏移量减600
};
// 开始定时器
function start() {
timer = setInterval(function () {
next.onclick();
}, 2000);
}
start();
// 关闭定时器
function stop() {
clearInterval(timer);
}
// 鼠标移出时,开始定时器
counter.onmouseleave = start;
// 鼠标移入时,关闭定时器
counter.onmouseenter = stop;
</script>
8.定时器的暂停和结束
(获取当前时间如何实现)
9.使用原生的方式实现 ajax 请求
10.css 手写轮播图:利用 animation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>轮播图</title>
</head>
<!-- style 里面是css样式 因为代码较少就没有再次创建 -->
<style>
/* 盒子1的样式 */
.box1 {
width: 722px;
height: 358px;
margin: 0 auto;
/* 超出的位置将隐藏并且不占位 */
overflow: hidden;
}
.box {
width: 2200px;
height: 352px;
animation: move 20s infinite;
}
@keyframes move {
0% {
transform: translateX(0);
}
30% {
transform: translateX(-722px);
}
60% {
transform: translateX(-1422px);
}
100% {
transform: translateX(-1422px);
}
}
.box img {
float: left;
height: 352px;
width: 722px;
}
</style>
<body>
<!--外层一个容器,里面是一个大长条包着几个小内容在滚动,每次translateX一个小内容的宽度即可,较为简单-->
<div class="box1">
<div class="box">
<img src="./images/img.jpg" alt="" />
<img src="./images/img2.jpg" alt="" />
<img src="./images/img3.jpg" alt="" />
</div>
</div>
</body>
</html>
渐变实现轮播图,更为简单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>轮播图</title>
</head>
<!-- style 里面是css样式 因为代码较少就没有再次创建 -->
<style>
/* 盒子1的样式 */
.focus {
margin: 0 auto;
width: 700px;
height: 322px;
animation: focus 20s infinite;
}
@keyframes focus {
0% {
background-image: url("./images/img.jpg");
}
50% {
background-image: url("./images/img2.jpg");
}
100% {
background-image: url("./images/img3.jpg");
}
}
</style>
<body>
<!--外层一个容器,里面通过background-image去变化即可-->
<div class="focus"></div>
</body>
</html>
11.手写 indexOf
indexOf
是 JavaScript 字符串和数组中的方法,用于查找给定值在字符串或数组中第一次出现的位置索引。以下是一个手写的 indexOf
函数示例,可以用于数组和字符串:
function myIndexOf(arrOrStr, target, startIndex = 0) {
if (Array.isArray(arrOrStr)) {
//数组的情况
for (let i = startIndex; i < arrOrStr.length; i++) {
//遍历查找匹配值的索引
if (arrOrStr[i] === target) {
return i;
}
}
} else if (typeof arrOrStr === "string") {
//字符串的情况
for (let i = startIndex; i < arrOrStr.length; i++) {
if (arrOrStr[i] === target) {
//注意:字符串也是可以直接用下标进行遍历的!
return i;
}
}
} else {
throw new TypeError("Unsupported data type");
}
return -1; //没有匹配值
}
// 测试数组
const arr = [1, 2, 3, 4, 5];
console.log(myIndexOf(arr, 3)); // 输出 2
// 测试字符串
const str = "Hello, world!";
console.log(myIndexOf(str, "o")); // 输出 4
// 测试未找到的情况
console.log(myIndexOf(arr, 6)); // 输出 -1
这个示例中,myIndexOf
函数接受一个数组或字符串、要查找的目标值以及一个可选的起始索引。它会遍历数组或字符串,从指定的起始索引开始,查找目标值第一次出现的位置索引。如果找到了,返回索引值;如果未找到,返回 -1。这是一个简化的手写示例,实际上内置的 indexOf
方法在处理一些特殊情况时可能会更加复杂。
12.手写 instanceof:遍历原型链 getPrototypeOf
instanceof
是 JavaScript 中用于检查一个对象是否是某个构造函数的实例的运算符。您可以通过检查对象的原型链来判断它是否是特定构造函数的实例。以下是一个手写的 instanceof
函数示例:
思路:大概就是看这个对象的原型是否指向了构造函数的原型对象(对象的原型对象是否是构造函数的原型对象)
注意:对象有可能是继承过来的,而不是由构造函数直接创建的,所以不能单单看一层的原型指向,而是要遍历对象的原型链,看它是否有某一层的原型对象是构造函数的原型对象!
其实遍历原型链就是遍历链表!
function myInstanceOf(obj, constructor) {
// 检查参数是否为对象和函数,如果不是的话,就直接失败
if (typeof obj !== "object" || obj === null) {
return false;
}
if (typeof constructor !== "function") {
throw new Error("Right-hand side of instanceof is not callable");
}
let prototype = Object.getPrototypeOf(obj); // 获取对象的原型
while (prototype !== null) {
if (prototype === constructor.prototype) {
//看对象的隐式原型是否指向构造函数的原型对象! ——> 关键点
return true;
}
prototype = Object.getPrototypeOf(prototype); //如果不是,就接着往原型链上层去获取原型对象 ——> 指针向后移动一位,遍历链表,也是关键点
}
return false;
}
// 测试示例
class MyClass {}
const obj = new MyClass();
console.log(myInstanceOf(obj, MyClass)); // true
console.log(myInstanceOf(obj, Array)); // false
在这个示例中,myInstanceOf
函数接受一个对象和一个构造函数作为参数,然后检查对象的原型链中是否存在与构造函数的原型相匹配的原型,如果有则返回 true
,否则返回 false
。
请注意,这只是一个简单的手写示例,实际上,内置的 instanceof
运算符在处理一些特殊情况时可能会更加复杂,比如处理原始值的包装对象等。
13.手写在原型链上给对象属性溯源的方法:类似手写 instanceof
实际上主要是用了:getPrototypeOf + hasOwnProperty
- 实现要求:如下效果,也就是说findPrototypeByProperty 函数可以帮我们寻找一个对象上面属性的源头是哪里(是在它原型链的哪个原型对象上面),实现这样一个 findPrototypeByProperty 函数
const foo = { a: 1 };
const bar = Object.create(foo); //bar的原型对象是foo
bar.b = 2;
const baz = Object.create(bar); //baz的原型对象是bar
baz.c = 3;
console.log(findPrototypeByProperty(baz, "c") === baz); // true
console.log(findPrototypeByProperty(baz, "b") === bar); // true
console.log(findPrototypeByProperty(baz, "a") === foo); // true
您想要实现一个函数 findPrototypeByProperty
,该函数会在原型链中寻找包含指定属性的对象,并返回。下面是一个实现的示例:
function findPrototypeByProperty(obj, prop) {
while (obj !== null) {
if (obj.hasOwnProperty(prop)) {
//hasOwnProperty是由对象实例调用,参数是一个属性名,判断这个属性是否直属于这个对象,而不是在原型链的!
return obj;
}
obj = Object.getPrototypeOf(obj); //获取对象的原型对象,也就是去到原型链的上一层
}
return null;
}
const foo = { a: 1 };
const bar = Object.create(foo);
bar.b = 2;
const baz = Object.create(bar);
baz.c = 3;
console.log(findPrototypeByProperty(baz, "c") === baz); // true
console.log(findPrototypeByProperty(baz, "b") === bar); // true
console.log(findPrototypeByProperty(baz, "a") === foo); // true
在这个示例中,findPrototypeByProperty
函数接受一个对象和一个属性名称作为参数。它会在给定的对象及其原型链上查找是否有包含指定属性的对象,并返回找到的对象。如果找不到包含该属性的对象,则返回 null
。函数使用了 hasOwnProperty
方法来检查属性是否存在于当前对象中,然后通过 Object.getPrototypeOf
方法获取对象的原型对象,从而遍历整个原型链。
请注意,如果多个对象的原型链上都包含指定属性,函数将返回第一个找到属性的对象。
13.手写数组扁平化(数组拍平):将嵌套数组转化为一维数组
**数组扁平化是指将多层嵌套的数组结构转换为一维数组。**以下是一些手写数组扁平化的方法示例:
- **递归方法:**类似手写对象的深拷贝
function flattenArrayRecursive(arr) {
let result = []; //每次递归都用一个新数组接收被拍了一次的数组
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flattenArrayRecursive(arr[i])); //拼接一个递归的拍平数组
} else {
result.push(arr[i]);
}
}
return result;
}
const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayRecursive(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
- 使用 reduce 方法优化(还是递归):
function flattenArrayWithReduce(arr) {
return arr.reduce((result, current) => {
//免去了for循环
if (Array.isArray(current)) {
return result.concat(flattenArrayWithReduce(current));
} else {
return result.concat(current);
}
}, []); //[]是初始数据
}
const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayWithReduce(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
- 使用 ES6 扩展运算符 + some 优化(避免递归):——> 最完美的方法
arr.some()
是 JavaScript 数组的一个方法,用于检查数组中是否至少有一个元素满足指定的测试函数。如果测试函数对任何一个元素返回true
,则some()
方法返回true
,否则返回false
。注意:
arr.every()
方法的作用和arr.some()
类似,不同的地方在于只有当数组中的所有元素都满足指定条件时,返回true
,否则返回false
。
arr.concat()
方法可以接收多个参数,这些参数可以是数组或值。它会将所有参数中的数组元素和值连接到调用它的数组中,并返回一个新的数组。以下是使用concat()
方法接收多个参数的示例:jsconst arr1 = [1, 2]; const arr2 = [3, 4]; const concatenatedArray = arr1.concat(arr2, 5, 6); console.log(concatenatedArray); // 输出 [1, 2, 3, 4, 5, 6] console.log(arr1); // 输出 [1, 2](原数组未改变) console.log(arr2); // 输出 [3, 4](原数组未改变)
在上述示例中,
concatenatedArray
包含了arr1
、arr2
、5
和6
这些参数中的元素。注意,concat()
方法不会改变原始数组arr1
和arr2
,而是返回一个新数组,该数组包含了所有连接的元素。注意:如果 concat 的参数里面有数组,会默认对数组进行…即展开,把[]去掉,再加入到前面的数组里面!
思路:利用 some 函数判断数组是否是嵌套数组,如果是的话利用 concat 会去掉[]的特性来实现每次去掉一层的效果!(也可以说是因为扩展运算符…去掉了一层[])
function flattenArrayWithSpread(arr) {
while (arr.some((item) => Array.isArray(item))) {
//arr.some(item => Array.isArray(item)) 可以用于检查数组 arr 中是否至少有一个元素是数组。如果数组中有一个元素是数组,则返回 true,否则返回 false。即判断是否有嵌套数组。
arr = [].concat(...arr); //利用concat可以接收多个参数(和push一样)和concat会把每一项参数的[]去掉的特性,实现了每次都可以去掉一层的效果!而...去掉的一层和前面的[]空数组抵消了!
}
return arr;
}
const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayWithSpread(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
这些方法都可以将多层嵌套的数组扁平化为一维数组。根据您的喜好和项目需求,可以选择适合您的方法。