最近面试比较多,经常被问到手写实现,不仅要写出来,还要懂为什么,说清楚。在这整理一下。
new
如何实现new?
new Object()创建一个空对象obj。- 取得构造函数,也就是
arguments的第一项。 - 将新对象的原型链接到传入的对象。新对象内部属性
__proto__指向构造函数的原型prototype,新对象就可以访问构造函数原型中的属性和方法。 - 使用
apply,将构造函数的this指向这个新对象,新对象就可以访问构造函数的属性和方法。 - 执行函数,取得返回值。如果返回值是一个对象,就返回该对象。否则返回
obj。
function mynew() {
let obj = new Object();
let Con = [].shift.call(arguments);
obj.__proto__ = Con.prototype;
let res = Con.apply(obj, arguments)
return typeof res == 'object' ? res : obj;
}
复制代码
注意:
[].shift.call(arguments)是删除并拿到arguments的第一项。
当
[].shift.call()传入arguments对象时,call改变了shift方法原来的this指向,指向了arguments,所以shift删除并拿到arguments的第一项。同理,将类数组对象为数组也是如此。
[].slice.call(arguments)等效于Array.prototype.slice.call(arguments)。(
slice()用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组)。
call,apply,bind
call,apply,bind第一个参数都是函数上下文this。
共同点是,都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。
改变执行上下文就是说,A 对象有一个方法,而 B 对象也需要用到同样的方法,那么这时候我们让B借用一下 A 对象的方法,既完成了需求,又减少了内存的占用。
所以说,B想用A的方法,利用
function B( ) {
A.call(this)
}
复制代码
apply 和 call 的区别是 call 方法第二个参数接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
apply 和 call 是立即执行的,
bind 是创建一个新的函数,我们必须要手动去调用。
实现call
注意他们都是函数原型上的方法~
- 取传入的对象context。(this 参数可以传 null,当为 null 的时候,视为指向 window。)
- 将函数设为对象的一个属性方法。也就是把这个要调用call的函数方法设为context对象的一个属性。
- 处理传入的参数。
- 传入参数,执行该方法。
- 删除该方法。
- 返回结果。
Function.prototype.myCall = function (context) {
var context = context || window;
context.fn = this;
let args = [...arguments].slice(1);
let result = context.fn(...args);
delete context.fn;
return result;
}
复制代码
实现apply
apply的第二个参数传入的是一个数组,所以需要判断是否存在,存在需要将数组展开。
Function.prototype.defineApply = function (context, arr) {
var context = context || window;
context.fn = this;
let result;
// 需要判断是否存在第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
复制代码
实现bind
bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
//用call、apply模拟实现bind
Function.prototype.mybind = function (context) {
let self = this; // 保存函数的引用
return function () { // 返回一个新的函数
// return self.apply(context, arguments);
return self.call(context, arguments);
}
};
复制代码
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))
}
}
复制代码
防抖节流
防抖
你尽管触发事件,但我都在事件触发后的n秒执行,以最新触发事件为准。
思路:
维护一个timer,记录当前状态。
如果当前存在定时器,就删除他,因为以最新的触发事件为准。
重新设置定时器并执行。
这里用到了闭包,同来保存timer。如果不使用闭包的话,每次return里面都重新设置了新的timer,不能找到上一次的删除。
应用场景:搜索联想、窗口resize、登录时不断点击
function debounce(fn,wait) {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
}
}
复制代码
节流
持续触发事件,一段时间内只执行一次。
思路:时间戳,先将初始值设为0。
如果当前时间减去之前的时间戳大于设置的等待时间,则执行函数。更新当前时间戳。
应用场景:滚动事件scroll、鼠标不断点击触发
function throttle(func,wait) {
let pre = 0;
return () => {
let cur = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (cur - pre > wait) {
func.apply(this, arguments);
pre = Date.now();
}
}
}
复制代码
深拷贝和浅拷贝
浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
浅拷贝的方式:
- Object.assign
- Array.prototype.slice()
- Array.prototype.concat()
- 扩展运算符
function slowclone(obj) {
let cloneobj = {};
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
cloneobj[k]=obj[k]
}
}
return cloneobj;
}
复制代码
注意:for in遍历,会遍历原型链里面的属性,所以使用hasOwnProperty排除原型链。
深拷贝
深拷贝开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
递归实现深拷贝:
遍历数组、对象,直到里面都是基本数据类型,然后再去复制。
- 如果不是对象或者是null,直接返回,不需要拷贝。
- 如果是Date或者正则,返回new的新实例。
需要注意:对象存在循环引用的问题,对象的属性引用了对象自身。所以可以额外开辟一个存储空间,存储当前对象和存储对象之间的关系。需要拷贝当前对象时,先去这个存储空间里找,有没有拷贝过这个对象,如果有,直接返回存储空间里的对象,没有的话继续拷贝。
function deepclone(obj, hash = new Map()) {
if (!obj || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
let cloneobj = obj.constructor();
if (hash.get(obj)) return hash.get(obj);
hash.set(obj, cloneobj);
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
cloneobj[k] = deepclone(obj[k], hash);
}
}
return cloneobj;
}
复制代码
instanceof
判断B是不是在A的原型链上。
不断地去找A的__proto__直到为B.prototype或者null。
function myinstanceof(A, B) {
let protoA = A.__proto__;
let prototypeB = B.prototype;
while (true) {
if (protoA == null) {
return false;
}
if (protoA == prototypeB) {
return true;
}
protoA = protoA.__proto__;
}
}
复制代码
Object.create()
用于创建一个新对象,被创建的对象继承另一个对象(o)的原型。所以F.prototype = o;
function createObj(o) {
//传入的参数o为返回实例的__porto__,也就是实例构造函数的显式原型
function F() {} //构造函数
F.prototype = o;
return new F(); //返回实例
}
复制代码
map
Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。
Array.prototype.newMap = function (fn) {
let newArr = [];
for (let i = 0; i < this.length; i++) {
newArr.push(fn(this[i], i, this)); //this指向调用newMap方法的数组
}
return newArr;
};
let arr = [1, 2, 3];
let res = arr.newMap((a) => a + 1);
console.log(res);
复制代码
reduce的参数如下
arr.reduce((previousValue, currentValue, currentIndex, array) => {}, initialValue)
复制代码
reduce实现map
Array.prototype.newMap = function (fn, Arg) {
var res = [];
this.reduce((prev, curr, index, array) => {
res.push(fn.call(Arg, curr, index, array));
}, 0); //指定初始值initialValue=0,所以从currentIndex=0开始,即第一个开始
return res;
};
复制代码
forEach
forEach()方法对数组的每个元素执行一次给定的函数。
arr.forEach(function(currentValue, currentIndex, arr) {}, thisArg)
//currentValue 必需。当前元素
//currentIndex 可选。当前元素的索引
//arr 可选。当前元素所属的数组对象。
//thisArg 可选参数。当执行回调函数时,用作 this 的值。
复制代码
Array.prototype._forEach = function(fn, thisArg) {
if (typeof fn !== 'function') throw "参数必须为函数";
if(!Array.isArray(this)) throw "只能对数组使用forEach方法";
let arr = this;
for(let i=0; i<arr.length; i++) {
fn.call(thisArg, arr[i], i, arr)
}
}
复制代码
Promise
• Promise 就是一个对象,用来表示并传递异步操作的最终结果。
解决回调函数层层嵌套产生的回调地狱问题。
Promise
function myPromise(executor) {
let self = this; //保留this。防止后面方法出现this指向不明的问题
self.status = 'pending';//promise的默认状态是pending
self.value = undefined;//保存成功回调传递的值
self.reason = undefined;//保存失败回调传递的值
self.successCB = [];//存储fulfilled状态对应的回调函数
self.failCB = [];//存储rejected状态对应的回调函数
function resolve(value) {
if (self.status === 'pending') { // 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
self.status = 'resolved';//成功函数将其状态修改为resolved
self.value = value;//将成功的值保存起来
self.successCB.forEach(fn=>fn());
}
}
function reject(reason) {
if (self.status === 'pending') { // 只能由pending状态 => rejected状态
self.status = 'rejected';//失败函数将其函数修改为rejected
self.reason = reason;//将失败的值保存起来
self.failCB.forEach(fn=>fn());
}
}
// 捕获在excutor执行器中抛出的异常
try {
executor(resolve,reject)
} catch (err) {
reject(err)
}
}
复制代码
Promise.prototype.then
then方法是原型链上的方法
myPromise.prototype.then = function (onResolved, onRejected) {
let self = this;
if (self.status === 'pending') {
self.successCB.push(() => {
onResolved(self.value);//将resolve函数保留的成功值传递作为参数
})
self.failCB.push(() => {
onRejected(self.reason);//将reject函数保留的失败值传递作为参数
})
}
if (self.status === 'resolved') {
onResolved(self.value);//将resolve函数保留的成功值传递作为参数
}
if (self.status === 'rejected') {
onRejected(self.reason);//将reject函数保留的失败值传递作为参数
}
}
复制代码
Promise.all
Promise.all 接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为resolve或者有一个reject状态出现的时候,它才会去调用.then方法,它们是并发执行的。
Promise.all()方法将多个Promise实例包装成一个Promise对象(p),接受一个数组(p1,p2,p3)作为参数,数组中不一定需要都是Promise对象,但是一定具有Iterator接口,如果不是的话,就会调用Promise.resolve将其转化为Promise对象之后再进行处理。
使用Promise.all()生成的Promise对象(p)的状态是由数组中的Promise对象(p1,p2,p3)决定的;
1、如果所有的Promise对象(p1,p2,p3)都变成fullfilled状态的话,生成的Promise对象(p)也会变成fullfilled状态,p1,p2,p3三个Promise对象产生的结果会组成一个数组返回给传递给p的回调函数;
2、如果p1,p2,p3中有一个Promise对象变为rejected状态的话,p也会变成rejected状态,第一个被rejected的对象的返回值会传递给p的回调函数。
function myall(promises) {
return new Promise((resolve, reject) => {//返回一个新的Promise
let ret = [];//定义一个空数组存放结果
let count = 0;
let done = (i, data) => {//处理数据函数
ret[i] = data;
count++;
if (count === promises.length) {//当i等于传递的数组的长度时
resolve(ret); //执行resolve,并将结果放入
}
}
for (let i = 0; i < promises.length; i++){
promises[i].then((data) => done(i, data), reject); //将结果和索引传入done函数
}
})
}
复制代码
Promise.race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
promise.race()中,promise的状态只能改变一次,即resolve和reject都只被能执行一次。
function myrace(promises) {
return new Promise(function (resolve, reject) {
for (let i = 0; i < promises.length; i++) {
promises[i].then(resolve, reject);
}
});
}
复制代码
柯里化
给函数分步传参,每次传递部分参数,并返回一个更具体的函数来接受剩余的参数。这中间可能接受多层这样的接受部分参数的函数,直至返回结果。
function curry(fn, args) {
var length = fn.length;//获取fn形参的个数
var args = args || [];//获取上一次的参数
return function(){//返回一个函数
//获取本次的参数并转换为数组
var newArgs = Array.prototype.slice.call(arguments);
newArgs=args.concat(newArgs);//将本次参数与上次的参数合并
if (newArgs.length < length) {//如果参数个数小于形参个数,继续收集
return curry.call(this,fn,newArgs);
}else{
return fn.apply(this,newArgs);//否则返回函数执行结果
}
}
}
复制代码
柯里化求和也很重要!
Object.prototype.toString() 方法返回一个表示该对象的字符串,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。
简单理解:在某个操作或者运算需要字符串而该对象又不是字符串的时候,会触发该对象的 String 转换,会将非字符串的类型尝试自动转为 String 类型。
function curyAdd(){
var args = Array.prototype.slice.call(arguments);
var adder = function () {
args.push(...arguments);
return adder;
}
adder.toString = function () {
return args.reduce((pre, cur) => {
return pre+cur;
},0)
}
return adder;
}
console.log(cury(1, 2)(3));
复制代码
Ajax
Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
}
// 请求的类型、请求的 url 以及是否异步发送请求
xhr.open("get", url, true);
// 传入请求的数据
xhr.send(null);
复制代码
俺是分割线~~~~
后续遇到其他的再补充!























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)