实现 new 过程:
要点:
- 函数第一个参数是构造函数
- 实例的__proto__指向构造函数的原型属性prototype
- 函数剩余参数要挂载到一个实例对象上
- 构造函数有返回值时,就返回这个返回值
const createObj = function () {
let obj = {}
let Constructor = [].shift.call(arguments) // 1
obj.__proto__ = Constructor.prototype // 2
let ret = Constructor.apply(obj, arguments) // 3
return typeof ret === 'object' ? ret: obj // 4
}
// 使用
const Fun = function (name) {
this.name = name
}
Fun.prototype.getName = function() {
alert(this.name)
}
let fun = createObj(Fun, 'gim')
fun.getName() // gim
值得注意的是,es6的class必须用new调用,否则会报错,如下:
class Fun {
constructor(name) {
this.name = name
}
getName() {
alert(this.name)
}
}
let fun = createObj(Fun, 'gim')
fun.getName() // Uncaught TypeError: Class constructor Fun cannot be invoked without 'new'
手写 call、apply 及 bind 函数
共同点:
- 第一个参数是要绑定的this
- 函数内部的 this 其实是要执行绑定的函数(因为三者都是点调用)
bind
这里实现简单版本(new 调用结果不一样)
- bind函数执行后,要返回一个原函数的拷贝
- 给返回函数内部的 fn 绑定传入的 context
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') throw 'caller must be a function'
const fn = this
return function() {
return fn.call(context, ...args, ...arguments)
}
}
call
、apply
函数的实现其实都借助了点调用。利用第一个参数做个中转,调用完之后删除。
call
Function.prototype.myCall = function(context = windows, ...args) {
context._fn = this
const result = context._fn(...args)
delete context._fn
return result
}
apply
Function.prototype.myApply = function(context = windows, args) {
context._fn = this
const result = context._fn(args)
delete context._fn
return result
}
参考面试题解答参见 前端手写面试题详细解答
节流和防抖
刚开始接触这俩概念的时候傻傻分不清楚。
浏览器的一些事件,如:resize,scroll,keydown,keyup,keypress,mousemove等。这些事件触发频率太过频繁,绑定在这些事件上的回调函数会不停的被调用。会加重浏览器的负担,导致用户体验非常糟糕。
节流防抖主要是利用了闭包。
节流
节流函数来让函数每隔 n 毫秒触发一次。
// 节流
function throttle (f, wait = 200) {
let last = 0
return function (...args) { // 以下 内部匿名函数 均是指这个匿名函数
let now = Date.now()
if (now - last > wait) {
last = now
f.apply(this, args) // 注意此时 f 函数的 this 被绑定成了内部匿名函数的 this,这是很有用的
}
}
}
// 未节流
input.onkeyup = funciton () {
$.ajax(url, this.value)
}
// 节流
input.onkeyup = throttle(function () { // throttle() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
$.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
})
防抖
防抖函数让函数在 n 毫秒内只触发最后一次。
// 防抖
function debounce (f, wait = 200) {
let timer = 0
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
f.apply(this, args)
}, wait)
}
}
// 未防抖
input.onkeyup = funciton () {
$.ajax(url, this.value)
}
// 防抖
input.onkeyup = debounce(function () { // debounce() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
$.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
})
柯里化函数
柯里化可以利用函数和不同的参数构成功能更加专一的函数。
柯里化其实就是利用闭包的技术将函数和参数一次次缓存起来,等到参数凑够了就执行函数。
function curry(fn, ...rest) {
const length = fn.length
return function() {
const args = [...rest, ...arguments]
if (args.length < length) {
return curry.call(this, fn, ...args)
} else {
return fn.apply(this, args)
}
}
}
function add(m, n) {
return m + n
}
const add5 = curry(add, 5)
Promise
要点:
- 三种状态的改变:
pending
fulfilled
rejected
-
resolve()
reject()
函数的实现 - 关键点
then
链式调用的实现
class MyPromise {
constructor(fn) {
this.status = 'pending'
this.value = null
this.resolve = this._resolve.bind(this)
this.reject = this._reject.bind(this)
this.resolvedFns = []
this.rejectedFns = []
try {
fn(this.resolve, this.reject)
} catch (e) {
this.catch(e)
}
}
_resolve(res) {
setTimeout(() => {
this.status = 'fulfilled'
this.value = res
this.resolvedFns.forEach(fn => {
fn(res)
})
})
}
_reject(res) {
setTimeout(() => {
this.status = 'rejected'
this.value = res
this.rejectedFns.forEach(fn => {
fn(res)
})
})
}
then(resolvedFn, rejecetedFn) {
return new MyPromise(function(resolve, reject) {
this.resolveFns.push(function(value) {
try {
const res = resolvedFn(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
resolve(res)
}
} catch (err) {
reject(err)
}
})
this.rejectedFns.push(function(value){
try {
const res = rejectedFn(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
reject(res)
}
} catch (err) {
reject(err)
}
})
})
}
catch(rejectedFn) {
return this.then(null, rejectedFn)
}
}
this.resolvedFns
和this.rejectedFns
中存放着 then
函数的参数的处理逻辑,待 Promise 操作有了结果就会执行。
then
函数返回一个Promise实现链式调用。
其实面试的时候主要靠死记硬背,因为有一次 20 分钟让我写 5 个实现(包括promise),,,谁给你思考的时间。。。
深拷贝
乞丐版的
function deepCopy(obj) {
//判断是否是简单数据类型,
if (typeof obj == "object") {
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for (let i in obj) {
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
} else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
观察者模式和发布订阅模式
观察者模式观察者Observer和主体Subject都比较清晰,而发布订阅模式的发布和订阅都由一个调度中心来处理,发布者和订阅者界限模糊。
观察者模式存在耦合,主体中存储的是观察者实例,而 notify
方法遍历时调用了观察者的 update
方法。而发布订阅模式是完全解耦的,因为调度中心中存的直接就是逻辑处理函数。
要点:都要实现添加/删除/派发更新三个事件。
观察者模式
class Subject {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
this.observers = [...new Set(this.observers)]
}
notify(...args) {
this.observers.forEach(observer => observer.update(...args))
}
remove(observer) {
let observers = this.observers
for (let i = 0, len = observers.length; i < len; i++) {
if (observers[i] === observer) observers.splice(i, 1)
}
}
}
class Observer {
update(...args) {
console.log(...args)
}
}
let observer_1 = new Observer() // 创建观察者1
let observer_2 = new Observer()
let sub = new Subject() // 创建主体
sub.add(observer_1) // 添加观察者1
sub.add(observer_2)
sub.notify('I changed !')
发布订阅模式
这里使用了还在提案阶段的 class
的私有属性 #handlers
,但是主流浏览器已支持。
class Event {
// 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
#handlers = {}
// 事件添加方法,参数有事件名和事件方法
addEventListener(type, handler) {
// 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
if (!(type in this.#handlers)) {
this.#handlers[type] = []
}
// 将事件存入
this.#handlers[type].push(handler)
}
// 触发事件两个参数(事件名,参数)
dispatchEvent(type, ...params) {
// 若没有注册该事件则抛出错误
if (!(type in this.#handlers)) {
return new Error('未注册该事件')
}
// 便利触发
this.#handlers[type].forEach(handler => {
handler(...params)
})
}
// 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
removeEventListener(type, handler) {
// 无效事件抛出
if (!(type in this.#handlers)) {
return new Error('无效事件')
}
if (!handler) {
// 直接移除事件
delete this.#handlers[type]
} else {
const idx = this.#handlers[type].findIndex(ele => ele === handler)
// 抛出异常事件
if (idx === -1) {
return new Error('无该绑定事件')
}
// 移除事件
this.#handlers[type].splice(idx, 1)
if (this.#handlers[type].length === 0) {
delete this.#handlers[type]
}
}
}
}
要点:
- 函数第一个参数是构造函数
- 实例的__proto__指向构造函数的原型属性prototype
- 函数剩余参数要挂载到一个实例对象上
- 构造函数有返回值时,就返回这个返回值
const createObj = function () {
let obj = {}
let Constructor = [].shift.call(arguments) // 1
obj.__proto__ = Constructor.prototype // 2
let ret = Constructor.apply(obj, arguments) // 3
return typeof ret === 'object' ? ret: obj // 4
}
// 使用
const Fun = function (name) {
this.name = name
}
Fun.prototype.getName = function() {
alert(this.name)
}
let fun = createObj(Fun, 'gim')
fun.getName() // gim
值得注意的是,es6的class必须用new调用,否则会报错,如下:
class Fun {
constructor(name) {
this.name = name
}
getName() {
alert(this.name)
}
}
let fun = createObj(Fun, 'gim')
fun.getName() // Uncaught TypeError: Class constructor Fun cannot be invoked without 'new'
手写 call、apply 及 bind 函数
共同点:
- 第一个参数是要绑定的this
- 函数内部的 this 其实是要执行绑定的函数(因为三者都是点调用)
bind
这里实现简单版本(new 调用结果不一样)
- bind函数执行后,要返回一个原函数的拷贝
- 给返回函数内部的 fn 绑定传入的 context
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') throw 'caller must be a function'
const fn = this
return function() {
return fn.call(context, ...args, ...arguments)
}
}
call
、apply
函数的实现其实都借助了点调用。利用第一个参数做个中转,调用完之后删除。
call
Function.prototype.myCall = function(context = windows, ...args) {
context._fn = this
const result = context._fn(...args)
delete context._fn
return result
}
apply
Function.prototype.myApply = function(context = windows, args) {
context._fn = this
const result = context._fn(args)
delete context._fn
return result
}
参考面试题解答参见 前端手写面试题详细解答
节流和防抖
刚开始接触这俩概念的时候傻傻分不清楚。
浏览器的一些事件,如:resize,scroll,keydown,keyup,keypress,mousemove等。这些事件触发频率太过频繁,绑定在这些事件上的回调函数会不停的被调用。会加重浏览器的负担,导致用户体验非常糟糕。
节流防抖主要是利用了闭包。
节流
节流函数来让函数每隔 n 毫秒触发一次。
// 节流
function throttle (f, wait = 200) {
let last = 0
return function (...args) { // 以下 内部匿名函数 均是指这个匿名函数
let now = Date.now()
if (now - last > wait) {
last = now
f.apply(this, args) // 注意此时 f 函数的 this 被绑定成了内部匿名函数的 this,这是很有用的
}
}
}
// 未节流
input.onkeyup = funciton () {
$.ajax(url, this.value)
}
// 节流
input.onkeyup = throttle(function () { // throttle() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
$.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
})
防抖
防抖函数让函数在 n 毫秒内只触发最后一次。
// 防抖
function debounce (f, wait = 200) {
let timer = 0
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
f.apply(this, args)
}, wait)
}
}
// 未防抖
input.onkeyup = funciton () {
$.ajax(url, this.value)
}
// 防抖
input.onkeyup = debounce(function () { // debounce() 返回内部匿名函数,所以 input 被绑定到了内部匿名函数的 this 上
$.ajax(url, this.value) // 注意这个 this 在执行时被 apply 到了内部匿名函数上的 this ,也就是 input
})
柯里化函数
柯里化可以利用函数和不同的参数构成功能更加专一的函数。
柯里化其实就是利用闭包的技术将函数和参数一次次缓存起来,等到参数凑够了就执行函数。
function curry(fn, ...rest) {
const length = fn.length
return function() {
const args = [...rest, ...arguments]
if (args.length < length) {
return curry.call(this, fn, ...args)
} else {
return fn.apply(this, args)
}
}
}
function add(m, n) {
return m + n
}
const add5 = curry(add, 5)
Promise
要点:
- 三种状态的改变:
pending
fulfilled
rejected
-
resolve()
reject()
函数的实现 - 关键点
then
链式调用的实现
class MyPromise {
constructor(fn) {
this.status = 'pending'
this.value = null
this.resolve = this._resolve.bind(this)
this.reject = this._reject.bind(this)
this.resolvedFns = []
this.rejectedFns = []
try {
fn(this.resolve, this.reject)
} catch (e) {
this.catch(e)
}
}
_resolve(res) {
setTimeout(() => {
this.status = 'fulfilled'
this.value = res
this.resolvedFns.forEach(fn => {
fn(res)
})
})
}
_reject(res) {
setTimeout(() => {
this.status = 'rejected'
this.value = res
this.rejectedFns.forEach(fn => {
fn(res)
})
})
}
then(resolvedFn, rejecetedFn) {
return new MyPromise(function(resolve, reject) {
this.resolveFns.push(function(value) {
try {
const res = resolvedFn(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
resolve(res)
}
} catch (err) {
reject(err)
}
})
this.rejectedFns.push(function(value){
try {
const res = rejectedFn(value)
if (res instanceof MyPromise) {
res.then(resolve, reject)
} else {
reject(res)
}
} catch (err) {
reject(err)
}
})
})
}
catch(rejectedFn) {
return this.then(null, rejectedFn)
}
}
this.resolvedFns
和this.rejectedFns
中存放着 then
函数的参数的处理逻辑,待 Promise 操作有了结果就会执行。
then
函数返回一个Promise实现链式调用。
其实面试的时候主要靠死记硬背,因为有一次 20 分钟让我写 5 个实现(包括promise),,,谁给你思考的时间。。。
深拷贝
乞丐版的
function deepCopy(obj) {
//判断是否是简单数据类型,
if (typeof obj == "object") {
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for (let i in obj) {
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
} else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
观察者模式和发布订阅模式
观察者模式观察者Observer和主体Subject都比较清晰,而发布订阅模式的发布和订阅都由一个调度中心来处理,发布者和订阅者界限模糊。
观察者模式存在耦合,主体中存储的是观察者实例,而 notify
方法遍历时调用了观察者的 update
方法。而发布订阅模式是完全解耦的,因为调度中心中存的直接就是逻辑处理函数。
要点:都要实现添加/删除/派发更新三个事件。
观察者模式
class Subject {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
this.observers = [...new Set(this.observers)]
}
notify(...args) {
this.observers.forEach(observer => observer.update(...args))
}
remove(observer) {
let observers = this.observers
for (let i = 0, len = observers.length; i < len; i++) {
if (observers[i] === observer) observers.splice(i, 1)
}
}
}
class Observer {
update(...args) {
console.log(...args)
}
}
let observer_1 = new Observer() // 创建观察者1
let observer_2 = new Observer()
let sub = new Subject() // 创建主体
sub.add(observer_1) // 添加观察者1
sub.add(observer_2)
sub.notify('I changed !')
发布订阅模式
这里使用了还在提案阶段的 class
的私有属性 #handlers
,但是主流浏览器已支持。
class Event {
// 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
#handlers = {}
// 事件添加方法,参数有事件名和事件方法
addEventListener(type, handler) {
// 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
if (!(type in this.#handlers)) {
this.#handlers[type] = []
}
// 将事件存入
this.#handlers[type].push(handler)
}
// 触发事件两个参数(事件名,参数)
dispatchEvent(type, ...params) {
// 若没有注册该事件则抛出错误
if (!(type in this.#handlers)) {
return new Error('未注册该事件')
}
// 便利触发
this.#handlers[type].forEach(handler => {
handler(...params)
})
}
// 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
removeEventListener(type, handler) {
// 无效事件抛出
if (!(type in this.#handlers)) {
return new Error('无效事件')
}
if (!handler) {
// 直接移除事件
delete this.#handlers[type]
} else {
const idx = this.#handlers[type].findIndex(ele => ele === handler)
// 抛出异常事件
if (idx === -1) {
return new Error('无该绑定事件')
}
// 移除事件
this.#handlers[type].splice(idx, 1)
if (this.#handlers[type].length === 0) {
delete this.#handlers[type]
}
}
}
}