
- call
- apply
- bind
- new
- 防抖
- 节流
- promise
- 排序算法(冒泡、快排 - 原地/非原地)
手写Function.call
call 的核心:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定
this到函数并传入给定参数执行函数 - 如果不传入参数,默认指向 window
实现
Function.prototype.myCall = function (newThis = window, ...args) {
newThis.func = this
const result = newThis.func(...args)
delete newThis.func
return result
}
缺点:这里默认newThis上没有func这个key,更好的做法是用Symbol
测试代码
window.value = 1
let foo = { value: 2 }
function bar (extra) {
console.log(extra, this.value)
}
bar('a') // 'a', 1
bar.myCall(foo, 'a') // 'a', 2
手写Function.apply
类似call,但传参方式不同。
实现
Function.prototype.myApply = function (newThis = window, argArray) {
newThis.func = this
const result = newThis.func(...argArray)
delete newThis.func
return result
}
缺点:这里默认newThis上没有func这个key,更好的做法是用Symbol
测试代码
window.value = 1
let foo = { value: 2 }
function bar (extra) {
console.log(extra, this.value)
}
bar('a') // 'a', 1
bar.myCall(foo, 'a') // 'a', 2
手写bind
注意点:
- 需要判断调用者是否为函数。
- 参数的传入,除了在
bind的时候传入,也可以在调用bind返回的函数时传入,这里要做参数的拼接。 - 一个绑定函数也能使用
new操作符创建对象:这种行为就像把原函数当成构造器,提供的this值被忽略,同时调用时的参数被提供给模拟函数。(测试用例3)
详解:https://github.com/yygmind/blog/issues/23
实现
v1:
Function.prototype.myBind = function (newThis = window, ...args) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("not a function");
}
const functionItSelf = this
return function (...insideArgs) {
functionItSelf.apply(newThis, [...args, ...insideArgs])
}
}
上面没有考虑new操作符的情况。
new操作符的执行过程可以看下面的手写New。
v2:
Function.prototype.myBind = function (newThis = window, ...args) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("not a function");
}
const functionItSelf = this
var boundFunction = function (...insideArgs) {
functionItSelf.apply(this instanceof boundFunction ? this : newThis, [...args, ...insideArgs])
}
boundFunction.prototype = this.prototype;
return boundFunction
}
上面实现中 boundFunction.prototype = this.prototype有一个缺点,直接修改 boundFunction.prototype 的时候,也会直接修改 this.prototype。
执行测试代码,最后一个会输出为modified!
v3:
上面提到的问题,其实就是引用了同一个this.prototype的引用。这里可以用Object.create(this.prototype)的方式拷贝一个。
Function.prototype.myBind = function (newThis = window, ...args) {
// 调用 bind 的不是函数,需要抛出异常
if (typeof this !== "function") {
throw new Error("not a function");
}
const functionItSelf = this
var boundFunction = function (...insideArgs) {
functionItSelf.apply(this instanceof boundFunction ? this : newThis, [...args, ...insideArgs])
}
boundFunction.prototype = Object.create(this.prototype); // 需要ES5+
return boundFunction
}
测试代码
window.value = 1
let foo = { value: 2 }
function bar (extra) {
console.log(extra, this.value)
}
bar.prototype.test = 'can modify?'
const test1 = bar.myBind(foo, 'a')
test1() // 'a' 2
const test2 = bar.myBind(foo)
test2('a') // 'a' 2
const test3 = new test2('a'); // 'a' undefined
test3.__proto__.test = 'modified!'
console.log(bar.prototype.test) // 'can modify?'
手写new
当代码 new Foo(...) 执行时,会发生以下事情:
- 一个继承自
Foo.prototype的新对象被创建。 - 使用指定的参数调用构造函数
Foo,并将this绑定到新创建的对象。new Foo等同于new Foo(),也就是没有指定参数列表,Foo不带任何参数调用的情况。 - 由构造函数返回的对象就是
new表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
实现
function myNew (...args) {
const newInstance = {} // 这里不能写Object.create(null),因为这样会失去原型(即prototype属性)
const contructorFunction = Array.prototype.shift.call(args)
newInstance.__proto__ = contructorFunction.prototype // 这一步就是继承,__proto__指向构造函数的原型
const result = contructorFunction.apply(newInstance, args) // 执行构造函数
return result instanceof Object ? result : newInstance // 上面的第三点
}
测试代码
function Car(color) {
this.color = color;
}
Car.prototype.start = function() {
console.log(this.color + " car start");
}
var car = myNew(Car, "black");
car.color; // 访问构造函数里的属性
// black
car.start(); // 访问原型里的属性
// black car start
手写throttle
throttle有三种实现的方式:
- 使用timestamp,记录上一次执行的时间。可以实现
{ trailing: false }的情况,但不能实现{ leading: false }的情况。 - 使用setTimeout,定时执行。可以实现
{ leading: false }的情况,但不能实现{ trailing: false }的情况。
Underscore或者lodash这种库,throttle函数默认都是{ leading: true, trailing: true }的。因此就要把这两个写法结合起来,才能实现。
注:throttle第三个参数可以传入this。不过一般面试时不考虑。
实现
// 方式1 - 只能实现 { leading: true, trailing: false }
function myThrottle (func, timeout) {
let lastExecTimestamp = 0
return (...args) => {
if (Date.now() - lastExecTimestamp > timeout) {
func.apply(this, args)
lastExecTimestamp = Date.now()
}
}
}
// 方式2 - { leading: false, trailing: true }
function myThrottle (func, timeout) {
let timer = null
return (...args) => {
if (timer === null) {
timer = setTimeout(() => {
func.apply(this, args)
timer = null
}, timeout)
}
}
}
// 方法2 变式 - { leading: true, trailing: false }
function myThrottle (func, timeout) {
let timer = null
return (...args) => {
if (timer === null) {
func.apply(this, args)
timer = setTimeout(() => {
timer = null
}, timeout)
}
}
}
// 完善版本
function myThrottle (func, timeout, option = { trailing: true, leading: true }) {
let timer = null
let lastExecTimestamp = 0
let trailExec = false
return (...args) => {
if (timer === null && Date.now() - lastExecTimestamp >= timeout) {
if (option.leading) {
func.apply(this, args)
}
lastExecTimestamp = Date.now()
timer = setTimeout(() => {
if (trailExec) {
func.apply(this, args)
lastExecTimestamp = Date.now()
trailExec = false
}
timer = null
}, timeout)
} else {
if (option.trailing) trailExec = true
}
}
}
测试代码
var a = myThrottle(() => console.log('exec'), 1000)
var b = myThrottle(() => console.log('exec'), 1000, { leading: true, trailing: false })
var c = myThrottle(() => console.log('exec'), 1000, { leading: false, trailing: true })
a();a(); // 立刻输出一条'exec',然后1s后输出第二条'exec'
b();b(); // 立刻输出且只输出一条'exec'
c();c(); // 1s后输出且只输出一条'exec'
手写 Debounce
Debounce跟throttle不同的地方就是只是延迟执行。
当然也可以设置leading和trailing。
实现
function myDebounce (func, timeout, option = { leading: false, trailing: true }) {
let timer = null
return (...args) => {
if (option.leading && timer === null) {
func.apply(this, args)
}
if (timer !== null) {
clearTimeout(timer)
}
timer = setTimeout(() => {
if (option.trailing) {
func.apply(this, args)
}
timer = null
}, timeout)
}
}
测试代码
var a = myThrottle(() => console.log('exec'), 1000)
var b = myThrottle(() => console.log('exec'), 1000, { leading: true, trailing: false })
a();a(); // 第二次调用a()的1s之后输出'exec'
b();b(); // 立刻输出'exec',间隔1s以上的调用才会重新触发输出'exec'
手写简单Promise
Promise/A+ 规范的基本要求:
- promise 有三个状态:pending,fulfilled,or rejected;「规范 Promise/A+ 2.1」
- new promise时, 需要传递一个executor()执行器,执行器立即执行;
- executor接受两个参数,分别是resolve和reject;
- promise 的默认状态是 pending;
- promise 有一个value保存成功状态的值,可以是undefined/thenable/promise;「规范 Promise/A+ 1.3」
- promise 有一个reason保存失败状态的值;「规范 Promise/A+ 1.5」
- promise 只能从pending到rejected, 或者从pending到fulfilled,状态一旦确认,就不会再改变;
- promise 必须有一个then方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」
- 如果调用 then 时,promise 已经成功,则执行onFulfilled,参数是promise的value;
- 如果调用 then 时,promise 已经失败,那么执行onRejected, 参数是promise的reason;
- 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调onRejected;
思路,分三步走:
- 实现简单的状态机,resolve函数,reject函数,then函数,注意this指向。
- 实现异步回调
- 实现链式调用
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor (executor) {
// 初始状态是pending
this.status = PENDING
// 成功的结果
this.result = undefined
// 失败的结果
this.reason = undefined
// 成功回调
this.resolveCallbacks = []
// 失败回调
this.rejectCallbacks = []
let resolve = (result) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.result = result
this.resolveCallbacks.forEach(fn => fn())
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.rejectCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
// 异步
if (this.status === PENDING) {
typeof onFulfilled === 'function' && this.resolveCallbacks.push(() => {
onFulfilled(this.result)
})
typeof onRejected === 'function' && this.rejectCallbacks.push(() => {
onRejected(this.reason)
})
}
// 同步
if (this.status === FULFILLED) {
typeof onFulfilled === 'function' && onFulfilled(this.result)
}
if (this.status === REJECTED) {
typeof onRejected === 'function' && onRejected(this.reason)
}
}
}