实现call、apply、bind
定义一个函数与一个对象
function Person(a, b, c) {
return {
name: this.name,
a: a,
b: b,
c: c,
};
}
var chain = {
name: "柴柴",
};
call
//原生js实现call函数,es6的解构写法,rest参数
Function.prototype.myCall = function (obj, ...params) {
var obj = obj || window;
obj.p = this; //形参对象添加p属性,里面是Person这个函数
let result = obj.p(...params); //执行这个函数
delete obj.p; //把这个方法删除,因为不能改写对象
return result;
};
const res = Person.myCall(chain, 1, 2, 3);
console.log("res", res);
apply
apply跟call类似,只是apply的参数放在一个数组里面,params表示数组里的所有参数
Function.prototype.myApply = function (obj, params) {
var obj = obj || window;
obj.p = this; //形参对象添加p属性,里面是Person这个函数
let result;
if (!params) {
//执行这个函数,
result = obj.p();
} else {
result = obj.p(...params); //...params展开数组里的参数
}
delete obj.p; //把这个方法删除,因为不能改写对象
return result;
};
const res = Person.myApply(chain, [1, 2, 3]);
console.log("res", res);
bind
bind与call和apply有点不一样,bind会返回一个函数,在函数里面执行call的方法
Function.prototype.myBind = function (obj, ...params) {
var _this = this; //this执行的是Person函数
var obj = obj || window;
return function () {
//容易造成this的丢失,要提前将this保存,在返回的函数里执行call和apply的功能
return _this.call(obj, ...params); //Person.call(obj)
};
};
const res = Person.myBind(chain, 1, 2, 3)();
console.log("res", res);
实现new
1.创建一个空对象
2.将obj的隐形原型指向构造函数的显示原型
3.将步骤1新创建的对象作为this的上下文
4.如果该函数没有返回对象(即result不是一个对象),则返回this(即obj)
//实现new
function myNew(fn, ...arg) {
//1.创建空对象
var obj = {};
//2.将obj的隐形原型指向构造函数的显示原型
obj.__proto__ = fn.prototype;
//3.将步骤1新创建的对象作为this的上下文
var result = fn.call(obj, ...arg);
//4.如果该函数没有返回对象(即result不是一个对象),则返回this(即obj)
return result instanceof Object ? result : obj;
}
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = myNew(Person, "柴柴", 12);
实现instanceof
instanceof运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置
instanceof运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。
1.左边是对象,右边是构造函数(可以查看原型链,其实Object也是一个构造函数)
2.迭代。左侧对象的__proto__不等于构造函数的prototype时,沿着原型链重新赋值左侧
//判断构造函数的prototype是否在对象的原型链上
//左边是对象,右边是构造函数(可以查看原型链,其实object也是一个构造函数)
function myInstanceof(L, R) {
//L是实例, R是构造函数
//构造函数的显示原型在实例对象的隐式原型链上
//1.首先判断类型,如果数据是基本类型,直接返回false
if (typeof L === "object" || typeof L === "function") {
//2.如果是引用类型,取出R的显示原型
let RP = R.prototype;
L = L.__proto__;
//递归遍历
while (L) {
if (L === RP) {
return true;
}
L = L.__proto__;
}
return false;
} else {
return false;
}
}
const res = myInstanceof(Function, Object);
console.log(res);
组合继承
在js高级教程博客中有详细介绍
组合继承=原型链继承+构造函数继承
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this.name, this.age);
}
Person.prototype.setName = function (name) {
this.name = name;
};
function Student(name, age, price) {
Person.call(this, name, age);
this.price = price;
}
//只需要两个语句
Student.prototype = new Person();
Student.prototype.constructor = Student; //手动修正Student.prototype.constructor
let result = new Student("柴柴", 5, 33);
result.setName("肥肥");
console.log(result);
浅拷贝、深拷贝、深度比较
浅拷贝:只考虑对象类型。
浅拷贝: 用 = 号赋值引用地址
js对于浅拷贝,改了一个对象的某个值以后,另外一个对象对应的属性值也会跟着改变
const oldObj = {
name: "柴柴",
age: 20,
colors: ["red", "pink", "white"],
frineds: {
name: "肥肥",
},
};
const newObj = oldObj;
newObj.name = "哈哈";
console.log("oldObj", oldObj);
console.log("newObj", newObj);
function shallowCopy(obj) {
if (typeof obj !== "object") return;
let newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return obj;
}
let obj = { name: "柴柴" };
let res = shallowCopy(obj);
console.log(res);
深拷贝:
//js对于浅拷贝,改了一个对象的某个值以后,另外一个对象对应的属性值也会跟着改变
const oldObj = {
name: "柴柴",
age: 20,
colors: ["red", "pink", "white"],
frineds: {
name: "肥肥",
},
};
//定义一个深拷贝函数
function deepClone(obj) {
//这个obj要么是对象,要么是数组
if (typeof obj !== "object" || obj === null) {
return obj;
}
let result;
//深拷贝过程
//1.定义格式 是数组还是对象
if (obj instanceof Array) {
result = [];
} else {
result = {};
}
for (let key in obj) {
//遍历这个obj的键名
if (obj.hasOwnProperty(key)) {
//只拷贝对象自身的属性
//result[key] = obj[key];不递归调用的话只会拷贝最外层
result[key] = deepClone(obj[key]);
}
}
return result;
}
const newObj = deepClone(oldObj);
newObj.name = "哈哈";
console.log("oldObj", oldObj);
console.log("newObj", newObj);
深拷贝JSON版本:
function deepClone(obj) {
let cloneObj = JSON.stringify(obj);
cloneObj = JSON.parse(cloneObj);
return cloneObj;
}
深度比较:判断两个对象是否相同
function isEqual(obj1, obj2) {
//如果有一个或都不是对象
if (typeof obj1 != "object" || typeof obj2 != "object") {
return obj1 === obj2;
} else if (obj1 === obj2) {
return true;
}
//不是同一个对象
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if (obj1Keys.length != obj2Keys.length) {
return false;
}
for (let key in obj1) {
//键名一样
const res = isEqual(obj1[key], obj2[key]);
if (!res) {
return false;
}
}
//都比较完了,并且没有false
return true;
}
防抖和节流
防抖:函数防抖,这里的抖动就是执行的意思,而一般的抖动都是持续的,多次的。假设函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。重点就是规定时间内一直按的话不会执行,冷静下来后才会执行。
1.持续触发不执行
2.在规定时间内未触发第二次,则执行
<body>
<input type="text" id="hInput" />
<script>
const inputDOM = document.getElementById("hInput");
//当用户输入完毕时,才发送一次http请求
function debounce(fn, delay) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer); //清除当前定时器
}
timer = setTimeout(() => {
fn(); //执行传入的函数
}, delay);
};
}
inputDOM.addEventListener(
"input",
debounce(() => {
console.log("发送请求");
}, 1000)
);
</script>
</body>
节流(一段时间只执行一个操作):
节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。一直按的话会每隔给定时间执行。
1.持续触发并不会执行多次,在规定时间内只触发一次
2.到一定时间再去执行
<body>
<div class="box" draggable="true"></div>
<!-- 节流:一段时间内,只执行一个某操作,过了这段时间,还有操作的话,继续执行新的操作 -->
<script>
const boxDom = document.querySelector(".box");
function throttle(fn,delay) {
let timer = null;
return function () {
if (timer) {
return;
}
//设置一个定时器
timer = setTimeout(() => {
fn();
timer = null;
}, delay);
};
}
boxDom.addEventListener(
"drag",
throttle(function (e) {
console.log("test");
},200)
);
</script>
</body>
事件代理与事件绑定
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<button id="btn">增加一项</button>
</ul>
<script>
//事件绑定
const list = document.getElementById("list");
const lis = list.getElementsByTagName("li");
const btn = document.getElementById("btn");
listArray = Array.prototype.slice.call(lis);
function bindEvent(elem, type, fn, selector) {
elem.addEventListener(type, (event) => {
const target = event.target;
if (selector === undefined) {
//普通绑定,直接执行fn
fn(event);
} else {
//事件代理,判断当前点击函数是否匹配
if (target.matches(selector)) {
fn(event);
}
}
});
}
//两种情况,第一种是事件代理,第二种是事件绑定
bindEvent(
list,
"click",
function (e) {
console.log(e.target.innerHTML);
},
"li"
);
bindEvent(list, "click", function (e) {
console.log(e.target.innerHTML);
});
btn.addEventListener("click", () => {
const li = document.createElement("li");
li.innerHTML = "新增";
list.insertBefore(li, btn);
});
</script>
</body>
手写promise
async function send() {
const result = await sendTo();
console.log("result", result);
}
function sendTo() {
let promise = new Promise((resolve, reject) => {
let data = { name: "柴柴" };
resolve(data);
});
promise
.then((res) => {
console.log(res.name);
})
.catch((err) => {
console.log(err);
});
return promise;
}
send();
js中的拷贝函数
1.直接赋值(引用地址):obj2 = obj1,obj2 = Object(obj1)
2.Object.assign():拷贝的也是引用地址,且不可枚举属性和继承属性不拷贝。
语法: Object.assign(target, …sources) target: 目标对象,sources: 源对象
用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, source);
let f = [1, 2, [3, 4, 5]];
let copy = Object.assign([], f);
3.String.prototype.slice():浅拷贝,二级属性还有联系
其实就相当于把String看成一个字符数组,然后调用Array.prototype.slice()方法
let f = [1, 2, [3, 4, 5]];
let copy = f.slice(0);
4.String.prototype.concat():浅拷贝,二级属性还有联系、
let f = [1, 2, [3, 4, 5]];
let copy = [].concat(f);
5.Array.from():浅拷贝
let f = [1, 2, [3,4,5]]
let g = Array.from(f)
g[2][0] = 0
console.log(f) // [ 1, 2, [ 0, 4, 5 ] ]
数组扁平化
1.直接使用flat(),当我们要明确知道它有多少层
[1, [2, [3]]].flat(2) // [1, 2, 3]
2.ES6实现
结合some、Array.isArray、…
some():方法用于检测数组中的元素是否有满足指定条件的,若满足返回true,否则返回false
var arr = [1, [2, [3]]];
//用es6实现
function flatArr(arr) {
//Array.isArray(item)判断当前这一项是不是数组
while (arr.some((item) => Array.isArray(item))) {
console.log(arr);
arr = [].concat(...arr);
}
return arr;
}
let res = flatArr(arr);
console.log(res);
数组去重
es5
function unique(arr) {
let res = arr.filter(function (item, index) {
//若当前值在数组中的下标的该元素的下标,因为后一个重复的元素,他查找到的元素下标返回的是第一个的
return arr.indexOf(item) === index;
});
return res;
}
let arr = [1, 2, 3, 4, 2];
let res = unique(arr);
console.log(res);
es6:
let arr = [1, 2, 3, 4, 2];
let res = [...new Set(arr)];
获取url参数并转化成对象
重点:了解正则表达式用法
//获取url参数并转化成对象
function getQueryParams() {
const res = {};
const queryString = "?from=search&seid=133&spm=333";
//获取相对应的key和value保存到res中
const reg = /[?&][^?&]+=[^?&]/g;
const found = queryString.match(reg);
if (found) {
found.forEach((item) => {
//substring(1)去掉第一个字符
let temp = item.substring(1).split("=");
let key = temp[0];
let value = temp[1];
res[key] = value;
});
}
return res;
}
let url = "https://wwwvideo/?from=search&seid=133&spm=333";
let res = getQueryParams(url);
console.log(res);
函数柯里化
把接收多个参数的函数变换成接收一个单一参数的函数(单一参数为多个参数中的第一个)
//因为参数不确定,所以不设置形参
function add() {
//并且把保存参数的arguments赋值给args变量保存起来,也就是第一个括号的函数
//定义一个数组用来存储所有的参数,将arguments对象转化成数组
let args = Array.prototype.slice.call(arguments);
let inner = function () {
//接收第二次传入的参数,也就是第二个括号,然后把第二个括号的参数加入到第一个括号的参数里面
args.push(...arguments);
//实际上,每次增加一个括号,我们都需要增加一个内部函数,用递归实现
//只需要在内部函数里面进行自己调用自己即可实现
return inner;
};
inner.toString = function () {
return args.reduce(function (prev, cur) {
return prev + cur;
});
};
return inner;
}
/*
执行第一个括号,返回inner函数(但返回的是字符串,原本的函数呗转换为字符串显示,原因是发生隐式转换调用toString)
执行第二个括号,返回inner函数
执行第三个括号,返回inner函数
。。。。
*/
alert(add(1)(2)(4));
console.log(add(1)(2)(4));
object.create()(原型式继承)
该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是一个浅拷贝。
function objcet (obj) {
function F () {};
F.prototype = obj;
return new F();
}
Object.createCopy = function (proto, prototypeObject = "undefined") {
//当proto不是函数并且不是对象的时候抛出错误
if (typeof proto !== "object" && typeof proto !== "function") {
throw new TypeError("Object prototype may only be an Object or null.");
}
function F() {}
F.prototype = proto;
const obj = new F();
if (prototypeObject != "undefined") {
Object.defineProperties(obj, prototypeObject);
}
if (proto == null) {
//创建一个没有原型对象的对象,object.create(null)
obj.__proto__ = null;
}
return obj;
};
var obj = { name: "柴柴" };
var fun = function () {};
var obj2 = Object.createCopy(fun);
console.log(obj2);
proxy代理
Proxy 也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,
说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。
proxy官网文档、handler用法
语法:
let p = new Proxy(target, handler);
target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。
例子1:
let obj = { name: "柴柴", age: 18 };
obj = new Proxy(obj , {
get(target, key) {
console.log('获取了getter属性');
return target[key];
}
});
console.log(obj.name);
上方的案例,我们首先创建了一个obj对象,里面有name属性,然后我们使用Proxy将其包装起来,再返回给obj,此时的obj已经成为了一个Proxy实例,我们对其的操作,都会被Proxy拦截。
Proxy有两个参数:
第一个是target,也就是我们传入的obj对象,
另一个则是handler,也就是我们传入的第二个参数,一个匿名对象。
在handler中定义了一个名叫get的函数,当我们获取 obj的属性时,则会触发此函数。
咱们再来试试使用set来拦截一些操作,并将get返回值更改
Reflect官方文档
obj = new Proxy(obj, {
get(target, key) {
let result = target[key];
//如果是获取 年龄 属性,则添加 岁字
if (key === "age") result += "岁";
return result;
},
set(target, key, value) {
if (key === "age" && typeof value != "number") {
//抛出错误
throw Error("age字段必须为Number类型");
}
return Reflect.set(target, key, value);
},
});
console.log(`我叫${obj.name} 我今年${obj.age}了`);
class SingleObject {
constructor(name) {
this.name = name;
}
single() {
console.log("单例");
}
}
function proxy(func) {
let instance;
let handler = {
construct(target, args) {
if (!instance) {
instance = Reflect.construct(func, args);
}
return instance;
},
};
return new Proxy(func, handler);
}
const SingleA = proxy(SingleObject);
const a1 = new SingleA("柴柴");
const a2 = new SingleA("肥肥");//返回的是a1
console.log(a1 === a2, a1, a2);
图片懒加载
如果用户还没看到网页下面的内容,在某种程度上我们就没必要这么快加载看不见的图片
滚动到网页下面才能预览到看不见的图片
/**
* 将img标签的src属性改成data-src:浏览器碰到这个属性不会像默认属性那样进行属性处理,相当于不知道在哪里加载这些图片
* 监听scroll这个事件,鼠标滚动就触发(但该方法非常)
* 知道两个高度-窗口显示区高度:window.innerHeight
* -图片到视窗上的距离:getBoundingClientRect().top
* 推荐IntersectionObserve:浏览器提供的构造函数,目标元素会和可视窗口会产生交叉区域
*
*/
const images = document.querySelectorAll("img");
//方法一:监听scroll
window.addEventListener("scroll", (e) => {
//遍历每一张图片,判断它图片到视窗上的距离与窗口显示区高度的关系
images.forEach((image) => {
const imageTop = image.getBoundingClientRect().top;
//如果图片距离视窗顶部的距离小于窗口显示区高度,就代表该图片可以看见了,使图片开始加载
if (imageTop < window.innerHeight) {
//获取刚刚取得的自定义属性,再把这个自定义属性赋值给原本的src属性
const data_src = image.getAttribute("data-src");
image.setAttribute("src", data_src);
}
console.log("scroll触发");
});
});
//方法二:IntersectionObserve,target指的是目标元素
const callback = (entries) => {
console.log("回调函数接收一个参数,是一个数组", entries);
entries.forEach((entry) => {
if (entry.isIntersecting) {
const image = entry.target;
const data_src = image.getAttribute("data-src");
image.setAttribute("src", data_src);
observe.unobserve(image);
console.log("触发");
}
});
};
const observe = new IntersectionObserver(callback);
//给每一个图片绑定一个观察构造函数
images.forEach((image) => {
observe.observe(image);
});
滚动加载
原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.content {
height: 200px;
}
.one {
background-color: pink;
}
.two {
background-color: rgb(236, 236, 120);
}
.there {
background-color: rgb(154, 184, 105);
}
.four {
background-color: rgb(74, 162, 168);
}
</style>
</head>
<body>
<div class="main">
<div class="content one"></div>
<div class="content two"></div>
<div class="content there"></div>
<div class="content four"></div>
</div>
</body>
<script>
window.addEventListener(
"scroll",
function () {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight + scrollTop >= scrollHeight) {
//监听滚动到最底部
const one = document.createElement("div");
one.setAttribute("class", "content one");
const two = document.createElement("div");
two.setAttribute("class", "content two");
const there = document.createElement("div");
there.setAttribute("class", "content there");
const four = document.createElement("div");
four.setAttribute("class", "content four");
const mainEle = document.querySelector(".main");
const fragment = document.createDocumentFragment();
fragment.append(one, two, there, four);
mainEle.appendChild(fragment);
}
},
false
);
</script>
</html>
插入几万条数据不卡页面
渲染大数据时,合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入的数据
const once = 20;
// 插入数据需要的次数
const loopCount = Math.ceil(total / once);
let countOfRender = 0;
const ul = document.querySelector("ul");
// 添加数据的方法
function add() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
const li = document.createElement("li");
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0);
setTimeout(() => {
const total = 100000;
const once = 20;
const loopCount = Math.ceil(total / once);
const ul = document.querySelector("ul");
let startCount = 0;
function loop() {
//先判断数据插入完成没有
if (startCount < loopCount) {
//插入数据
window.requestAnimationFrame(add);
}
}
loop();
function add() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
let li = document.createElement("li");
li.innerHTML = Math.floor(Math.random() * total);
fragment.append(li);
}
ul.appendChild(fragment);
startCount++;
loop();
}
}, 0);
字符串解析
主要用replace和eval函数
eval() 函数计算 JavaScript 字符串,并把它作为脚本代码来执行。
replace语法:str.replace(regexp|substr, newSubStr|function)
参数:
regexp :正则表达式 substr :字符串。仅第一个匹配项会被替换。 newSubStr
(replacement):用于替换掉第一个参数匹配到的字符串。 function
(replacement):一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。
返回值:一个部分或全部匹配由替代模式所取代的新的字符串。
var a = {
b: "123",
c: "456",
e: "789",
};
var strs = `a{a.b}aa{a.c}aa {a.e}aaaa`;
function str(str) {
const reg = /{(\w\.\w)}/g;
str = str.replace(reg, (x, y) => eval(y));
return str;
}
const res = str(strs);
console.log(res);
lookeup
function lookup(dataObj, keyName) {
// 看看keyName中有没有点符号,但是不能是.本身
if (keyName.indexOf(".") != -1 && keyName != ".") {
// 如果有点符号,那么拆开
var keys = keyName.split(".");
// 设置一个临时变量,这个临时变量用于周转,一层一层找下去。
var temp = dataObj;
// 每找一层,就把它设置为新的临时变量
for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]];
}
return temp;
}
// 如果这里面没有点符号
return dataObj[keyName];
}
var res = lookup(
{
m: {
n: {
p: 100,
},
},
},
"m.n.p"
);
console.log(res);