参考文章:
【javascript】手写call,apply,bind函数
本文从以上文章中,摘录总结了一部分常见且需要好好理解一下的JS手写代码题型,面试必备,按需选取
包括以下这些,更多更详细的解释可查阅以上原文出处
- call
- apply
- bind
- new
- 数组扁平化
- 数组去重
- 原型继承
- promise、promise.all、promise.race
- instanceof
- ajax
- 闭包cache
- 浅拷贝、深拷贝
- 字符串转驼峰
- 图片懒加载
- 滚动加载
一、JS实现一个call
方法或函数fun.call(obj, 参数1,参数2,...),第一个值是改变this指向到obj,后面是参数队列,调用call立即执行方法fun
call的定义和用法
// call方法第一个参数指的是this的指向;接受一个参数列表;方法立即执行
// Function.prototype.call()样例
function fun(arg1, arg2) {
console.log(this.name)
console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 这里把fun里的this,指向对象_this,然后立即执行,由此才可以输出YIYING
fun.call(_this, 1, 2)
// 输出
YIYING
3
手写实现call
Funcion.protoType.mockCall = function (context = window, ...args) {
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
或者:
Function.prototype.myCall = function(context) {
if (typeof context === "undefined" || context === null) {
context = window
}
//context=context||window 和上面的代码一样
context.fn = this//(因为call的调用方式形如:myFun.call(obj),因此此时call方法的this指向为myFun,因此context.fn = this即为context.fn = myFun)
const args = [...arguments].slice(1)//第一个参数为context,要去除
const result = context.fn(...args)
delete context.fn
return result
}
实现分析
- 首先context为可选参数,如果不传的话默认上下文是window
- 接下来给content创建一个独一无二的属性(Symbol表示),并将值设置为需要调用的函数
- 因为call可以传入多个参数作为调用函数的参数,这里用的...扩展运算符
- 然后调用函数并将对象上的函数删除
二、JS实现一个apply
方法或函数fun.apply(obj, [参数1,参数2,...]),改变this指向到obj,立即执行方法fun
apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个**参数数组。**apply和call实现类似,不同的就是参数的处理
Function.protoType.mockApply = function (context = window, args) {
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
三、JS实现一个bind
Function.prototype.bind 第一个参数是this的指向,从第二个参数开始是接收的参数列表。和call的区别在于bind方法返回值是函数以及bind接收的参数列表的使用。
实现思路:
- 利用闭包保存调用bind时的this,这时的this就是原函数
- 使用call/apply指定this
- 返回一个绑定函数
- 当返回的绑定函数被new运算符调用的时候,绑定的上下文指向new运算符创建的对象
- 将绑定函数的prototype修改为原函数的prototype
Function.protoType.mockBind = function (context = window, ...initArgs) {
const foo = this
var bindFoo = function (...args) {
if(this instanceof bindFoo){
return new fn(...initArgs, ...args)
}
return foo.call(context, ...initArgs, ...args)
}
return bindFoo
}
简写:
Function.prototype.mockBind = function(ctx){
let fn = this
return function(){
fn.apply(ctx, arguments) //arguments是函数调用时所传参数
}
}
第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。
四、手写一个new的实现
正常使用new
function Dog(name){
this.name = name
}
Dog.prototype.sayName = function(){
console.log('名字', this.name)
}
var dog1 = new Dog('小狗')
dog1.sayName() // 输出名字 小狗
思考一下new 操作符做了哪些事情?
- 创建一个新对象
- 新对象会被执行
__proto__链接,关联到构造函数的.prototype属性上,即和构造函数用的一个原型,从而可调用原型上的方法 - 函数调用的this绑定到新对象上
- 如果函数没有返回其他对象,那么new表达式中的函数会调用自动返回这个新对象
手写new实现
function mockNew (foo, ...args) {
if (typeof foo !== 'function') {
throw Error('foo is not a constructor')
}
const obj = Object.create(foo.protoType)
const result = foo.apply(obj, args)
return typeOf result === 'object' && result !== null ? result : obj
}
new的具体步骤
1. 创建一个空对象 var obj = {}
2. 修改obj.__proto__=Dog.prototype
3. 只改this指向并且把参数传递过去,call和apply都可以
4. 根据规范,返回 null 和 undefined 不处理,依然返回obj
五、数组扁平化
方法一:es6 flat方法
var arr = [1,2,[3,4,[5,6,[7]]]]
arr.flat(Infinity) // [1,2,3,4,5,6,7]
方法二:递归
var flatArr = function(arr1) {
let newArr = [];
function getChild(arr) {
for(let i = 0; i<=arr.length;i++) {
if(arr[i] instanceof Array === false && arr[i]) {
newArr.push(arr[i])
} else if(arr[i]){
getChild(arr[i])
}
}
}
getChild(arr1);
return newArr;
}
// 调用:
var a = [[1,2,2], [6,7,8, [11,12, [12,13,[14]]], 10]];
console.log('水电费', flatArr(a))
// [1, 2, 2, 6, 7, 8, 11, 12, 12, 13, 14, 10]
方法三:正则
const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
res2.map(item=> parseInt(item))
六、数组去重
方法一:es6 Set
var arr = [1,2,3,3,4,4,5]
var newArr = Array.from(new Set(arr)); // [1,2,3,4,5]
// 或者arr = [...set] Array.from() 将伪数组转换为数组
方法二:循环遍历数组
function filterArr(arr){
var newArr = [];
arr.forEach(item => {
if(!newArr.includes(item)) { // 也可以是!newArr.indexOf(item)
newArr.push(item)
}
})
return newArr
}
方法三:hash表
let arr = [1,1,2,3,2,1,2]
function unique(arr){
let obj = {}
arr.forEach((item) => {
obj[item] = true
})
let keys = Object.keys(obj)
keys = keys.map(item => parseInt(item)) // 转为数字
return keys
}
console.log(unique(arr))
七、原型继承(寄生组合继承)
这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
八、手写一个Promise
完整解析(写的很好,值得一看):
zhuanlan.zhihu.com/p/103651968
function Promise(executor) {
let self = this
this.status = 'pending' //当前状态
this.value = undefined //存储成功的值
this.reason = undefined //存储失败的原因
this.onResolvedCallbacks = []//存储成功的回调
this.onRejectedCallbacks = []//存储失败的回调
function resolve(value) {
if (self.status == 'pending') {
self.status = 'resolved'
self.value = value
self.onResolvedCallbacks.forEach(fn => fn());
}
}
function reject(error) {
if (self.status == 'pending') {
self.status = 'rejected'
self.reason = error
self.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
Promise.prototype.then = function (infulfilled, inrejected) {
let self = this
let promise2
infulfilled = typeof infulfilled === 'function' ? infulfilled : function (val) {
return val
}
inrejected = typeof inrejected === 'function' ? inrejected : function (err) {
throw err
}
if (this.status == 'resolved') {
promise2 = new Promise(function (resolve, reject) {
//x可能是一个promise,也可能是个普通值
setTimeout(function () {
try {
let x = infulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
if (this.status == 'rejected') {
promise2 = new Promise(function (resolve, reject) {
//x可能是一个promise,也可能是个普通值
setTimeout(function () {
try {
let x = inrejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
}
if (this.status == 'pending') {
promise2 = new Promise(function (resolve, reject) {
self.onResolvedCallbacks.push(function () {
//x可能是一个promise,也可能是个普通值
setTimeout(function () {
try {
let x = infulfilled(self.value)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
self.onRejectedCallbacks.push(function () {
//x可能是一个promise,也可能是个普通值
setTimeout(function () {
try {
let x = inrejected(self.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (err) {
reject(err)
}
});
})
})
}
return promise2
}
function resolvePromise(p2, x, resolve, reject) {
if (p2 === x && x != undefined) {
reject(new TypeError('类型错误'))
}
//可能是promise,看下对象中是否有then方法,如果有~那就是个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {//为了防止出现 {then:11}这种情况,需要判断then是不是一个函数
let then = x.then
if (typeof then === 'function') {
then.call(x, function (y) {
//y 可能还是一个promise,那就再去解析,知道返回一个普通值为止
resolvePromise(p2, y, resolve, reject)
}, function (err) {
reject(err)
})
} else {//如果then不是function 那可能是对象或常量
resolve(x)
}
} catch (e) {
reject(e)
}
} else {//说明是一个普通值
resolve(x)
}
}
Promise.all
Promise.all是支持链式调用的,本质上就是返回了一个Promise实例,通过resolve和reject来改变实例状态。
Promise.myAll = function(promiseArr) {
return new Promise((resolve, reject) => {
const ans = [];
let index = 0;
for (let i = 0; i < promiseArr.length; i++) {
promiseArr[i]
.then(res => {
ans[i] = res;
index++;
if (index === promiseArr.length) {
resolve(ans);
}
})
.catch(err => reject(err));
}
})
}
Promise.race
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
// 如果不是Promise实例需要转化为Promise实例
Promise.resolve(p).then(
val => resolve(val),
err => reject(err),
)
})
})
}
九、实现检测数据类型的instanceof
left表示要检测的数据,right表示类型。其原理是用原型链实现的,
A(实例对象) instanceof B(构造函数)。
function instanceof(left, right){
let proto = left._proto_
let prototype = right.prototype
while(true){
if(proto === null) return false
if(proto === prototype) return true
proto = proto._proto_
}
}
十、ajax
(1)get请求的ajax
let xhr = new XMLHttpRequest() //1、创建连接
xhr.open('GET', url, true) //2、连接服务器
xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
if (xhr.readyState === 4) {
if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
success(JSON.parse(xhr.responseText))
} else {
fail(xhr.status)
}
}
}
xhr.send(null) //3、发送请求
(2)post请求的ajax
let xhr = new XMLHttpRequest() //1、创建连接
const postData = {
userName: 'zhangshan',
passWord: 'xxx'
}
xhr.open('POST', url, true) //2、连接服务器
xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
if (xhr.readyState === 4) {
if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
success(JSON.parse(xhr.responseText))
} else {
fail(xhr.status)
}
}
}
xhr.send(JSON.stringify(postData)) //3、发送请求(需发送字符串,将json转化成字符串)
(3)用Promise优化
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest() //1、创建连接
xhr.open('GET', url, true) //2、连接服务器
xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
if (xhr.readyState === 4) {
if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
resolve(JSON.parse(xhr.responseText))
}else if(xhr.status === 404){
reject(new Error('404'))
}
}
}
xhr.send(null) //3、发送请求
})
}
const url = ''
ajax(url)
.then(res => console.log(JSON.parse(xhr.responseText)))
.catch(err => console.log(err))
十一、闭包写一个cache工具
function creatCache() {
let data = {} //隐藏数据,外部访问不到
return {
get(key) {
return data[key]
},
set(key, val) {
data[key] = val
}
}
}
var c = creatCache()
c.set('name', 'jin')
console.log(c.get('name'))
十二、浅拷贝、深拷贝
浅拷贝只复制对象的第一层属性、深拷贝是对对象的属性进行递归复制。
//浅拷贝 (obj1为所要拷贝的对象)
//方式一:原始版本(obj1为所要拷贝的对象,obj2已经默认为一个对象)
function shallowCopy(obj1, obj2){
for(let key in obj1){
obj2[key] = obj1[key]
}
}
//方式一:优化版本(obj为所要拷贝的对象)
function shallowClone(obj){
if(typeof obj !== 'object' || obj == null){
//obj是null,或者不是对象和数组,直接返回
return obj
}
let result
if(obj instanceof Array){
result = []
}else{
result = {}
}
for(let key in obj){// for in 遍历对象可枚举属性,包括其原型的属性和方法,
if(obj.hasOwnProperty(key)){ //保证key不是原型的属性
//递归调用
result[key] = obj[key]
}
}
//返回结果
return result
}
//方式二
function shallowCopy(obj1, obj2){
obj2 = Object.assign({}, obj1)
}
//深拷贝 (obj1为所要拷贝的对象)
//方式一:原始版本(obj1为所要拷贝的对象)
function deepCopy(obj1, obj2){
for(let key in obj1){// for in 遍历对象可枚举属性,包括其原型的属性和方法, 可用obj1.hasOwnPerporty(key)判断这个实例是否有这个属性
let item = obj1[key]
if(item instanceof Array){ // 不能用typeof item,因为不能区分对象和数组
obj2[key] = []
deepCopy(item, obj2[key])
}else if(item instanceof Object){
obj2[key] = {}
deepCopy(item, obj2[key])
}else{
obj2[key] = item
}
}
}
//方式一:优化版本(obj为所要拷贝的对象,obj2已经默认为一个对象)
思路:1、判断是否是值类型还是引用类型。2、判断是数组还是对象。3、递归
function deepClone(obj){
if(typeof obj !== 'object' || obj == null){
//obj是null,或者不是对象和数组,直接返回
return obj
}
let result
if(obj instanceof Array){
result = []
}else{
result = {}
}
for(let key in obj){// for in 遍历对象可枚举属性,包括其原型的属性和方法,
if(obj.hasOwnProperty(key)){ //保证key不是原型的属性
//递归调用
result[key] = deepClone(obj[key])
}
}
//返回结果
return result
}
缺陷:当遇到两个互相引用的对象,会出现死循环的情况。
//方式二
function deepCopy(obj1, obj2){
obj2 = JSON.parse(JSON.stringify(obj1))
}
缺陷:这种方法不能拷贝函数属性
十三、字符串转驼峰
方法一:分割成数组,利用toUpperCase()转大写,substring(1)为第一个元素后面的元素
var str="border-bottom-color";
function tf(){
var arr=str.split("-");
for(var i=1;i<arr.length;i++){
arr[i]=arr[i].charAt(0).toUpperCase()+arr[i].substring(1);
}
return arr.join("");
};
tf(str);
方法二:正则
var str="border-bottom-color";
function tf(){
var re=/-(\w)/g;
str=str.replace(re,function($0,$1){
return $1.toUpperCase();
});
alert(str)
};
tf(str);
十四、图片懒加载
可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。
function lazyload() {
const imgs = document.getElementsByTagName('img');
const len = imgs.length;
// 视口的高度
const viewHeight = document.documentElement.clientHeight;
// 滚动条高度
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
for (let i = 0; i < len; i++) {
const offsetHeight = imgs[i].offsetTop;
if (offsetHeight < viewHeight + scrollHeight) {
const src = imgs[i].dataset.src;
imgs[i].src = src;
}
}
}
// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);
十五、滚动加载
原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight + scrollTop >= scrollHeight) {
// 检测到滚动至页面底部,进行后续操作
// ...
}
}, false);