前言
本文只是对面试常用的源码实现进行了一次总结,提供了具体的实现方法,巩固下js知识,希望对你有所帮助。
1、实现一个简单的路由Router
- ** hash 版 **
// html
<a href="/index">首页</a>
<a href="/about">关于</a>
<div id="content"></div>
<script>
function Router() {
// 存放路由
this.routers = {}
// 当前hash
this.currentUrl = ''
this.init()
}
Router.prototype = {
// 定制路由规则
route: function (path, callback) {
this.routers[path] = callback || function () {}
},
// 刷新路由
refresh: function () {
this.currentUrl = location.hash.slice(1) || '/'
this.routers[this.currentUrl] && this.routers[this.currentUrl]()
},
// 窗口监视
init: function () {
window.addEventListener('load', this.refresh.bind(this), false)
window.addEventListener('hashchange', this.refresh.bind(this), false)
}
}
// 调用
var router = new Router()
router.route('/index', function () {
// todo
})
router.route('/about', function () {
// todo
})
</script>
- ** history 版 **
let aAll = document.getElementsByTagName('a'),
content = document.getElementById('content');
[].slice.call(aAll).forEach(item => {
item.addEventListener('click', function (e) {
// 阻止默认事件
e.preventDefault()
// 获取对应属性
let href = e.target.getAttribute('href')
// 添加一条记录,并保存到state中
history.pushState({
pathname: href
}, '', href)
// 根据href进行一些视图渲染
content.innerHTML = `<div>${href.slice(1)}组件</div>`
}, false)
})
// 监听popstate事件,一般浏览器前进后退时触发
window.addEventListener('popstate', function (e) {
// 根据e.state更新视图
content.innerHTML = `<div>${e.state.pathname.slice(1)}组件</div>`
}, false)
** 两者比较:**
-** hash模式 **
1、留下个#, 不够美观, 有代码洁癖的可以使用history模式,但这种模式兼容性较好
2、http请求不会携带#后的信息,改变hash也不会重新刷新页面
-** history模式 **
3、不能在本地直接打开文件使用,要在服务端环境下使用
4、利用了HTML5 History API拓展中新增的pushState() 和 replaceState() 方法。 pushState() 会向历史记录中添加一条记录,replaceState()对历史记录进行修改,虽然改变了当前的url,但浏览器不会立即向后端发送请求
5、可以自由的修改path, 但服务器中没有相应的响应或者资源的时候,返回返回一个404
2、手写一个call与apply
- ** call **
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('error')
}
// 如果没有指向window
context = context || window
// 将调用call函数的对象添加到context的属性中
context.fn = this
// 获取参数
const args = [...arguments].slice(1)
// 执行该属性
const result = context.fn(...args)
// 删除该属性
delete context.fn
// 返回结果
return result
}
// 调用
let call0 = function() {
console.log(this.name, arguments)
}
call0.prototype.name = 'jack';
let call1 = {
name: 'liLei'
}
call0.myCall(call1, 1, 2, 3) // liLei [1, 2, 3]
- ** apply **
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('error')
}
// 如果没有指向window
context = context || window
// 将调用call函数的对象添加到context的属性中
context.fn = this
// 获取参数
const args = arguments[1]
// 执行该属性
const result = context.fn(...args)
// 删除该属性
delete context.fn
// 返回结果
return result
}
// 调用
let apply0 = function() {
console.log(this.name, arguments)
}
a.prototype.name = 'jack';
let apply1 = {
name: 'liLei'
}
apply0.myApply(apply1, [1, 2, 3]) // liLei [1, 2, 3]
call与apply 只有参数的区别,第一个参数相同,call()后面是一串参数列表,apply()只有两个参数,第二个参数是个数组
3、手写一个bind
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('error')
}
// 保存this
let _this = this
// 获取参数
const args = Array.prototype.slice.call(arguments, 1)
// 返回一个函数
return function Fn() {
if (this instanceof Fn) {
return new _this(...arg, ...arguments)
} else {
return _this.apply(context, arg.concat(...arguments))
}
}
}
// 调用
let bind0 = function() {
console.log(this.name, arguments)
}
bind0.prototype.name = 'bar';
let bind1 = {
name: 'foo'
}
bind0.myBind(bind1, 1, 2, 3)() // foo [1, 2, 3]
call、aplly、bind核心都是改变this的指向,不同点call、aplly是直接调用了函数,而bind 是返回一个新的函数, 需要手动去调用
4、实现new操作
要实现new操作,我们要知道通过new关键字来调用构造函数经历了哪些
1、创建一个新的空对象
2、对象的 __ proto__ 属性指向构造函数的prototype指向的对象
3、已新建的对象为上下文,执行构造函数中代码为这个对象添加属性
4、如果构造函数有返回值且是对象,则返回该对象。否则返回新建的对象
function myNew() {
// 创建一个新的对象
let newObj = {}
// 获取外部传入的构造器
let _constructor = Array.prototype.shift.call(arguments)
// 新的对象隐式原型指向构造器的显示原型
newObj.__proto__ = _constructor.prototype
// 调用构造器, 改变其this指向
let result = _constructor.apply(newObj, arguments)
// 如果构造函数返回值是对象则返回这个对象,如果不是对象则返回新的对象
if (result && typeof result === 'function' || typeof result === 'object') {
return result;
}
return newObj
}
// 调用
new0 = function(name) {
this.name = name;
};
let newObj = myNew(new0, 'foo');
console.log(newObj); // { name: "foo" }
console.log(newObj instanceof new0); // true
5、实现instanceof
// 原理:left的__proto__是不是等于right.prototype,如果不等于再找
// left.__proto__.__proto__ 直到__proto__为null为止
function myInstanceof(left, right) {
let leftVal = left.__proto__
let rightVal = right.prototype
while (true) {
if (leftVal === null) return false
if (leftVal === rightVal) return true
leftVal = leftVal.__proto__
}
}
// 测试
function Person() {}
let foo = new Person()
console.log(myInstanceof(foo, Person)) // true
6、Object.create原理
// 将传入的对象作为原型
function myCreate(obj) {
function Fn() {}
Fn.prototype = obj
return new Fn()
}
7、浅拷贝
浅拷贝只复制地址值,实际上还是指向同一堆内存中的数据
// 1、 ...实现
let obj0 = {...{a: 1, b: 2}} // {a: 1, b: 2}
这里需要注意...只能对第一层进行深拷贝,再多一层就无法深拷贝了,所以还是属于浅拷贝
let obj1 = {a: 1, b: { c: 3 } }
let obj2 = {...obj1 }
console.log(obj2.b.c = 10) // 10
console.log(obj1.b.c) // 10
上述栗子可以看出obj2修改c的值时候,原来的obj1中的c也影响了
// 2、 Object.assign()
let bar = Object.assign({}, {a: 1, b: 2}) // {a: 1, b: 2}
8、深拷贝
深拷贝则是重新创建了一个相同的数据,二者指向的堆内存的地址值是不同的。这个时候修改赋值前的变量数据不会影响赋值后的变量
// 1、简单的函数递归拷贝
function deepCopy(obj) {
let copy = obj instanceof Array ? [] : {}
for (let item in obj) {
if (obj.hasOwnProperty(item)) {
copy[item] = typeof obj[item] === 'object' ? deepClone(obj[item]) : obj[item]
}
}
return copy
}
// 测试
let arr = [{
id: 1004,
name: "jack",
age: '22'
},
{
id: 1002,
name: "alice",
age: '2162'
},
{
id: 1003,
name: "haha",
age: '2232'
}
]
let result = deepCopy(arr)
console.log(result[0].name = 'jason') // jason
console.log(arr[0].name) // jack
这里没有
// 2、JSON.parse / JSON.stringify
let obj = {name: 'foo', age: 20}
let copy = JSON.parse(JSON.stringify(obj)) // {name: 'foo', age: 20}
JSON.parse / JSON.stringify 这种方法也是有弊端的,例如拷贝的数据比较大,会比较耗时,如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象,如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null等,这里不做展开了,详情可以看看这篇文章 blog.csdn.net/u013565133/…
9、实现一个简单的双向绑定
- ** Object.defineProperty()版 **
// html
<input placeholder="请输入" id="input"></input>
<span id="span"></span>
<script>
const data = {
text: 'this is text'
}
let input = document.getElementById('input'),
span = document.getElementById('span')
Object.defineProperty(data, 'text', {
set(Val) {
input.value = Val
span.innerHTML = Val
},
get() {
console.log('数据更新了')
}
})
// 触发视图更新
input.addEventListener('keyup', function (e) {
proxy.text = e.target.value
})
</script>
- ** Proxy()版 **
// html
<input placeholder="请输入" id="input"></input>
<span id="span"></span>
<script>
const data = {
text: 'this is text'
}
let input = document.getElementById('input'),
span = document.getElementById('span')
const handler = {
set(target, key, value) {
target[key] = value
input.value = value
span.innerHTML = value
return value
}
}
const proxy = new Proxy(data, handler)
// 触发视图更新
input.addEventListener('keyup', function (e) {
proxy.text = e.target.value
})
</script>
** 两者比较 **
-
Object.defineProperty主要问题在于,无法监听数组变化,准确的说是不支持数组的API,而proxy可以支持数组的API
-
Object.defineProperty的作用是劫持对象的属性,劫持的为getter和setter方法,是在对象属性变化时执行操作,proxy是劫持整个对象
-
Proxy会返回一个代理对象,我们只需要操作新对象即可,而Object.defineProperty只能遍历对象属性直接修改
-
Proxy有13种方法可以调用, 比Object.defineProperty要更加丰富的多
-
Object.defineProperty的兼容性要比Proxy要好
10、实现一个数组flat
const arr = [7, 2, 8, 4, [5, [{
name: 'jason',
age: 20
}, 10, [{
test: 'string'
}]], ]]
// 这里使用reduce()来实现
const flat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
}, []);
}
flat(arr) // [7, 2, 8, 4, 5, {name: 'jason', age: 20 }, 10, { test:'string' }]
11、实现一个简单的Promise
// 初始myPromise
function myPromise(constructor) {
let _this = this
// 定义初始状态
_this.status = 'pending'
// 定义resolved状态
_this.value = undefined
// 定义rejected状态
_this.reason = undefined
// resolved回调收集
_this.onFullfilledArr = []
// reject回调收集
_this.onRejectedArr = []
// resolve方法
function resolve(value) {
// 保证状态不可以逆转
if (_this.status === 'pending') {
_this.value = value
_this.status = 'resolved'
_this.onFullfilledArr.forEach(function (e) {
e(_this.value)
})
}
}
// reject方法
function reject(reason) {
// 保证状态不可以逆转
if (_this.status === 'pending') {
_this.reason = reason
_this.status = 'rejected'
_this.onRejectedArr.forEach(function (e) {
e(_this.reason)
})
}
}
// 捕获构造异常
try {
constructor(resolve, reject)
} catch (e) {
reject(e)
}
}
// 添加then 方法
myPromise.prototype.then = function (onFullfilled, onRejected) {
let _this = this,
_promise
switch (_this.status) {
case "pending":
_promise = new myPromise(function (resolve, reject) {
_this.onFullfilledArr.push(function () {
try {
let temple = onFullfilled(_this.value)
resolve(temple)
} catch (e) {
reject(e)
}
})
_this.onRejectedArr.push(function () {
try {
let temple = onRejected(_this.reason)
resolve(temple)
} catch (e) {
reject(e)
}
})
})
break;
case 'resolved':
_promise = new myPromise(function (resolve, reject) {
try {
let temple = onFullfilled(_this.value)
resolve(temple)
} catch (e) {
reject(e)
}
})
break;
case "rejected":
_promise = new myPromise(function (resolve, reject) {
try {
resolve(onRejected(_this.reason))
} catch (e) {
reject(e)
}
})
break;
default:
}
return _promise
}
// 测试
let p = new myPromise(function (resolve, reject) {
setTimeout(function () {
resolve(10)
}, 1000)
})
p.then(function (value) {
console.log(value) // 10
}).then(function () {
console.log('链式调用1') // 链式调用1
}).then(function () {
console.log('链式调用2') // 链式调用2
})
12、 数组去重
let arr = [1, 2, 3, 3, 6, 7, 2, 5, 6, 7, 1, 10]
// 1. new Set()
let result = [...new Set(arr)]
// 2、indexOf
function duplicate(array) {
let arr = []
for (const item of array) {
if (arr.indexOf(item) === -1) {
arr.push(item)
}
}
return arr
}
// 3、splice
function duplicate(array) {
let len = array.length
for (var i = 0; i < len; i++) {
for (var j = i + 1; j < len; j++) {
if (array[i] === array[j]) {
array.splice(j, 1)
j--;
}
}
}
return array
}
// 4、filter
function duplicate(array) {
return array.filter((item, index) => {
return array.indexOf(item) === index
})
}
// 5、hasOwnProperty
function duplicate(array) {
var obj = {};
return array.filter((item, index, arr) => {
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
13、实现一个防抖与节流
- ** 节流 **:是函数不管被触发多少次,都会在规定的时间内去执行一次处理函数
// 这种是利用时间戳,在规定的时间内触发
function throttle(fn, delay) {
// 获取触发时间戳
let prev = Date.now()
return function () {
// 保存
let context = this
// 获取参数
let args = arguments
// 获取当前时间戳
let now = Date.now()
// 对比时间
if (now - prev >= delay) {
fn.apply(context, args)
// 重置触发时间
prev = Date.now()
}
}
}
// 这个是定时器触发,在设定时间内触发
fucntion throttle(fn, delay) {
// 设置初始状态
let timer = null
return funtion() {
// 保存
let context = this;
// 获取参数
let args = arguments;
if (!timer) {
// 赋值定时器
timer = setTimeout(function () {
// 改变指向
fn.apply(context, args);
// 清空
timer = null;
}, delay);
}
}
}
- ** 防抖 **: 在一定时间内多次触发事件,只会执行一次处理函数,如果在规定时间内再次触发,先清除之前的定时器,重新计时
function debounce (fn, delay) {
// 设置初始状态
let timer = null
return function () {
let context = this
let args = arguments
// 如果在规定时间内再次触发,先清除定时器
if(timer) clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
14、实现一个柯里化函数
function myCurry() {
// 获取参数,转成数组
// 这里还是可以这样转换
// 1、Array.prototype.slice.call(arguments)
// 2、[].slice.call(arguments)
// 3、Array.from(arguments)
let args = [...arguments];
// 收集所有参数
let addfun = function () {
args.push(...arguments);
return addfun;
}
// 利用toString隐式转换的特性,返回最终值
addfun.toString = function () {
return args.reduce((a, b) => {
return a + b;
});
}
return addfun;
}
// 调用
myCurry(1, 2, 3) // 6
myCurry(1, 2, 3)(4) //10
最后
如有问题,欢迎指出,后续会持续更新!!!