[TOC]
# 實現 call、apply、bind
## 實現 call
首先要明白 call 是干什么的:call 方法在使用一個指定的 this 和若干個指定的參數值的前提下調用某個函數或方法
① 如何改變 this 指向?
- 將函數設置為對象的屬性
- 執行該函數
- 刪除該函數
delete 運算符可以刪除對象的屬性,但是不能刪除一個變量或者函數
② 如何把參數傳給要執行的函數并使其執行,且傳入的參數個數是不確定的?
- 運用 rest 運算符,或者使用 arguments 對象
``` js
Function.prototype.call2 = function (context, ...args) {
// 首先要獲取調用 call 的函數,用 this 可以獲取
context.fn = this
context.fn(...args)
delete context.fn // delete 刪除對象的屬性
}
let foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
bar.call2(foo, 'kevin', 18) // kevin 18 1
```
## 實現 apply
和 call 基本一樣,只是參數變為數組了而已
``` js
Function.prototype.apply2 = function (context, arr) {
// 首先要獲取調用 call 的函數,用 this 可以獲取
context.fn = this
context.fn(...arr)
delete context.fn // delete 刪除對象的屬性
}
let foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
bar.apply2(foo, ['kevin', 18]) // kevin 18 1
```
## 實現 bind
首先也是搞明白 bind 的作用是什么:bind() 方法會創建一個新函數,當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,傳遞參數時,可以在 bind 時傳遞部分參數,調用時又傳遞其余參數
完整的 bind 還考慮了:一個綁定函數也能使用 new 操作符創建對象:這種行為就像把原函數當成構造器。提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。
下面的代碼只實現 bind 的部分功能:
``` js
Function.prototype.myBind = function (context, ...args1) {
if (typeof this !== 'function') { // 只允許函數調用 bind 方法
throw new Error('Error')
}
let self = this
let firstArgs = args1
return function (...args2) {
return self.apply(context, firstArgs.concat(args2))
}
}
```
更完整的 bind 實現參考:[https://github.com/mqyqingfeng/Blog/issues/12](https://github.com/mqyqingfeng/Blog/issues/12)
# Promise 封裝原生 AJAX
``` js
let AJAX = function (url) {
let promise = new Promise(function (resolve, reject) {
let XHR = new XMLHttpRequest()
XHR.open('get', url, false) // false:異步 true:同步
XHR.onreadystatechange = handler
XHR.send()
function handler() {
if (XHR.readyState !== 4) {
return
}
if (XHR.status === 200) {
resolve(XHR.response)
} else {
reject(new Error(XHR.statusText))
}
}
})
return promise
}
```
XMLHttpRequest 的一些屬性和方法可參考:[AJAX 整理](http://www.hmoore.net/chenmk/web-knowledges/1106858)
# 函數節流、防抖 + immediate版
參考 [https://github.com/mqyqingfeng/Blog/issues/22](https://github.com/mqyqingfeng/Blog/issues/22)
防抖和節流的作用都是防止函數多次調用,區別在于,假設一個用戶一直觸發這個函數,且每次觸發函數的時間間隔小于 wait,防抖的情況下只會調用一次,而節流的情況會每隔一定時間調用函數
``` js
// 防抖
function debounce(fn, wait) {
let timer = 0
return function (...args) {
if (timer) clearTimeout(timer) // 清除并重新設置定時任務
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
```
但是需要注意的是,因為需要防抖一般是瀏覽器事件,那么我們就需要確保使用 debounce 處理過的事件處理程序不能出現一些意外的錯誤,比如 this 指向和 event 對象
```js
// 解決 this 和 event 不正確的問題
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
```
接下來再考慮一個立即執行的需求:我不希望非要等到事件停止觸發后才執行,我希望立刻執行函數,然后等到停止觸發 n 秒后,才可以重新觸發執行
```js
// 加上 immediate 參數表示是否需要立即執行
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已經執行過,不再執行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null; // 考慮到不再觸發之后的情況,如果不置 timeout 為null 就無法再實現下次的立即執行!
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
```
節流的簡單實現,詳細的可閱讀:[https://github.com/mqyqingfeng/Blog/issues/26](https://github.com/mqyqingfeng/Blog/issues/26)
``` js
// 節流,不會清除定時任務,而是延緩其執行
function throttle(fn, interval) {
let canRun = true
return function (...args) {
if (!canRun) return // 如果 interval 時間內再次觸發會在此處阻攔
canRun = false
setTimeout(() => {
canRun = true
fn.apply(this, args) // this 指向這個節流函數的調用者
}, interval)
}
}
```
這么寫有個明顯的問題,事件會在 interval 秒后才首次執行,假設有這么個需求,以鼠標在元素中移動為例,我們想要鼠標移入時立即執行,停止觸發時還能執行一次,可以這么寫:(代碼出自冴羽的博客,我還沒咋看懂)
```js
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
//下次觸發 func 剩余的時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果沒有剩余的時間了或者你改了系統時間
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
```
還有些其他需求,比如有頭無尾的,簡單修改下第一版即可:
```js
function throttle(fn, interval) {
let canRun = true
return function (...args) {
let context = this
if (!canRun) return // 如果 interval 時間內再次觸發會在此處阻攔
canRun = false
setTimeout(() => {
canRun = true
// fn.apply(this, args) // this 指向這個節流函數的調用者
}, interval)
fn.apply(context, args)
}
}
```
你可以用這么 demo 來試驗下:
```html
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
container.onmousemove = throttle(getUserAction, 1000)
function throttle(fn, interval) {
let canRun = true
return function (...args) {
let context = this
if (!canRun) return // 如果 interval 時間內再次觸發會在此處阻攔
canRun = false
setTimeout(() => {
canRun = true
// fn.apply(this, args) // this 指向這個節流函數的調用者
}, interval)
fn.apply(context, args)
}
}
</script>
</body>
</html>
```
# 手寫Promise
完整實現:[https://github.com/forthealllight/blog/issues/4](https://github.com/forthealllight/blog/issues/4)
簡易版實現(不能實現鏈式調用),忽略了一些細節(A+ 規范)
三句話總結:
- resolve 將狀態變為 fulfilled 并調用成功的回調
- reject 將狀態變為 rejected 并調用失敗的回調
- 原型上的 then 方法將成功的回調函數和失敗的回調函數添加到相應的隊列
```js
const PENDING = 'pending'
const FULFILLED= 'fulfilled'
const REJECTED = 'rejected'
function MyPromise (fn) {
const that = this // 緩存當前 promise 實例對象
this.state = PENDING // 初始狀態
this.value = null
this.resolvedCallbacks = []
this.rejectedCallbacks = []
function resolve (value) {
if(that.state === PENDING) {
that.state = FULFILLED
that.value = value
that.resolvedCallbacks.forEach(cb => cb(that.value))
}
}
function reject (value) {
if(that.state === PENDING){
that.state = REJECTED
that.value = value;
that.rejectedCallbacks.forEach(cb => cb(that.value));
}
}
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
//對傳入的兩個參數做判斷,如果不是函數將其轉為函數
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v // onFulfilled = v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r }
if (this.state === PENDING) {
this.resolvedCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
} else if (this.state === FULFILLED) {
onFulfilled(this.value)
} else {
onRejected(this.value)
}
}
// 測試
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('成功的回調數據')
}, 1000)
}).then(value => {
console.log('Promise.then: ', value)
})
```
模擬`Promise.all`和`Promise.race`的實現:
```js
/**
* Promise.all Promise 進行并行處理
* 參數: promise 對象組成的數組作為參數
* 返回值: 返回一個 Promise 實例
* 當這個數組里的所有 promise 對象全部變為 resolve 狀態的時候,才會 resolve。
*/
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let done = gen(promises.length, resolve)
promises.forEach((promise, index) => {
promise.then(value => {
done(index, value)
}, reject)
})
})
}
function gen (length, resolve) {
let count = 0
let values = [] // 被緩存
return function (i, value) {
values[i] = value
if (++count === length) {
console.log(values)
resolve(values)
}
}
}
/**
* Promise.race
* 參數: 接收 promise 對象組成的數組作為參數
* 返回值: 返回一個 Promise 實例
* 只要有一個 promise 對象進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行后面的處理(取決于哪一個更快)
*/
Promise.race = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise.then(resolve, reject)
})
})
}
```
# 函數柯里化
在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數且返回結果的新函數的技術。
**函數柯里化的主要作用和特點就是參數復用、提前返回和延遲執行。**
簡單來說,思路就是參數不夠的話就存儲參數并返回一個函數準備接收新的參數,參數夠了的話就執行。
``` js
function curry(fn, args1 = []) { // 默認值設為[],防止訪問args1.length報錯
return fn.length === args1.length ? fn.apply(null, args1) : function (...args2) {
return curry(fn, args1.concat(args2))
}
}
// fn(a,b,c,d)=>fn(a)(b)(c)(d)
// fn(a,b,c,d)=>fn(a, b)(c)(d)
function add(a, b, c, d) {
console.log(a + b + c + d)
}
let curryAdd = curry(add)
curryAdd(1, 2)(3)(4) // 10
curryAdd(1)(2)(3)(4)
curryAdd(1, 2, 3, 4)
```
另一種常見的問題
```js
/*
實現一個add方法,使計算結果能夠滿足如下預期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
*/
function add() {
// 第一次執行時,定義一個數組專門用來存儲所有的參數
var _args = Array.prototype.slice.call(arguments);
// 在內部聲明一個函數,利用閉包的特性保存 _args 并收集所有的參數值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用 toString 隱式轉換的特性,當最后執行時隱式轉換,并計算最終的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9
```
也可以重寫`valueOf`方法,調用`valueOf`方法時才返回值
```js
function add (...args) {
// 在內部聲明一個函數,利用閉包的特性收集參數
let _args = args
let _adder = function (...adderArgs) {
_args.push(...adderArgs)
return _adder // 返回自身,這樣就可以一直接受參數,直到調用 valueOf 方法
}
_adder.valueOf = function () {
return _args.reduce((prev, curr) => {
return prev + curr
})
}
return _adder
}
console.log(add(1, 2, 3)(2)(3).valueOf()) // 11
```
# 深淺拷貝
## 深拷貝
`JSON.parse(JSON.stringnify(object))`的不足:
* 會忽略?undefined
* 會忽略?symbol
* 不能序列化函數
* 不能解決循環引用的對象
自己實現一個深拷貝
``` js
function deepCopy (obj) {
let newObj
if (typeof obj === 'object') { // 數組或對象 typeof 會返回'object'
newObj = Array.isArray(obj) ? [] : {}
for(let key in obj) {
if (obj.hasOwnProperty(key)) {
// 如果對象的屬性仍然為對象/數組則遞歸
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
}
}
} else {
newObj = obj // 簡單數據類型直接復制
}
return newObj
}
```
測試一下
``` js
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function () {},
name: 'cxk'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // { name: 'cxk' }
let c = deepCopy(a)
console.log(c) // { age: undefined, sex: Symbol(male), jobs: [Function: jobs], name: 'cxk' }
```
# 字符串相關
## 1.字符串轉駝峰
``` js
// 字符串轉駝峰,注意第一個字符不大寫,如border-bottom-color -> borderBottomColor
function Change (str) {
let arr = str.split('-') // 字符串分割為數組
// map 返回由新的項組成的數組
arr = arr.map((item, index) => {
return index === 0 ? item : item.charAt(0).toUpperCase() + item.substring(1) // 第一個字符轉大寫與之后的字符合并
})
return arr.join("") // 數組合并為字符串
}
console.log(Change('border-bottom-color'))
```
## 2.查找字符串中出現次數最多的字符
用 map 試一下,map['a'] 存儲字符 a 出現的次數
``` js
// sdddrtkjsfkkkasjdddj 出現的最多的字符是 d
function mostChar (str) {
let map = new Map()
let max = 0
let maxChar
for (let i = 0; i < str.length; i++) {
let item = str[i]
if (!map.has(item)) {
map.set(item, 1)
} else {
let count = map.get(item)
map.set(item, count + 1)
if (count > max) {
max = count
maxChar = item
}
}
}
console.log(...map)
return maxChar
}
```
用對象的屬性來存儲試一下,maxNumber 存儲最大次數,maxChar存儲對應的字符
``` js
// 查找字符串中出現次數最多的字符的次數
// sdddrtkjsfkkkasjdddj 出現的最多的字符是 d
function findMostChars (str) {
let maxNumber = 0, maxChar
let obj = {}
for (let i = 0; i < str.length; i++) {
const char = str[i]
if (!obj[char]) {
obj[char] = 1
} else {
obj[char]++
if (obj[char] > maxNumber) {
maxChar = char
maxNumber = obj[char]
}
}
}
return maxChar
}
```
# 數組相關
## 1.數組扁平化 5種方法
``` js
const arr = [1, [2, 3, [4, 5]]]
function flattern(arr) {
return arr.reduce((preValue, currValue, currIndex, array) => {
return preValue.concat(Array.isArray(currValue) ? flattern(currValue) : currValue)
}, []) // []作為第一個preValue
}
// toString & split
function flattern2(arr) {
return arr.toString().split(',').map(item => {
return Number(item) // split分隔后數組元素為字符串形式, 需要轉換為數值形式
})
}
// PS:該方法及下一種方法都有一個問題,即[1, '1', 2, '2']的結果是[1, 1, 2, 2],我們改變了原數據(類型),這是不應該的
// join & split
function flattern3(arr) {
return arr.join(',').split(',').map(item => { // join方法和toString方法效果一樣?
return parseInt(item)
})
}
// concat遞歸
function flattern4(arr) {
let res = []
arr.map(item => {
if (Array.isArray(item)) {
res = res.concat(flattern(item))
} else {
res.push(item)
}
})
return res
}
// 擴展運算符
function flattern5(arr) {
while (arr.some(item => Array.isArray(item))) { // 如果數組元素中有數組
arr = [].concat(...arr) // [].concat(...[1, 2, 3, [4, 5]]) 擴展運算符可以將二維數組變為一維的
}
return arr
}
```
如果只是將二維數組扁平化為一維數組的話也可以這樣寫
``` js
function flattern(arr) {
return Array.prototype.concat.apply([], arr)
}
```
## 2.數組亂序
思路:從最后一個數開始,每次將當前遍歷位置的數與其之前的隨機一個數交換(random index實現),然后將這個位置視為已排序的,依次往前重復執行該過程。
注意 Math.random() 的范圍是 [0, 1)
``` js
let x = [1, 2, 3, 4, 5];
function shuffle(arr) {
var length = arr.length,
randomIndex,
temp;
while (length) {
randomIndex = Math.floor(Math.random() * (length--));
temp = arr[randomIndex];
arr[randomIndex] = arr[length];
arr[length] = temp
}
return arr;
}
console.log(shuffle(x))
```
## 3.數組去重
參考:[https://github.com/mqyqingfeng/Blog/issues/27](https://github.com/mqyqingfeng/Blog/issues/27)
嘗試寫一個名為 unique 的工具函數,我們根據一個參數 isSorted 判斷傳入的數組是否是已排序的,如果為 true,我們就判斷相鄰元素是否相同,如果為 false,我們就使用 indexOf 進行判斷
```js
var array1 = [1, 2, '1', 2, 1];
var array2 = [1, 1, '1', 2, 2];
// unique 工具函數用于數組去重
function unique(array, isSorted) {
var res = [];
var seen = [];
for (var i = 0, len = array.length; i < len; i++) {
var value = array[i];
if (isSorted) { // 如果數組是排序過的,判斷當前元素與前一個元素是否相等
if (!i || seen !== value) {
res.push(value)
}
seen = value;
}
else if (res.indexOf(value) === -1) { // 如果數組是未排序的,使用 indexOf 方法
res.push(value);
}
}
return res;
}
console.log(unique(array1)); // [1, 2, "1"]
console.log(unique(array2, true)); // [1, "1", 2]
```
在數組中元素有一些特殊類型如對象、NaN 的情況下,不同的數組去重方法可能會有不同的返回值
```js
// demo1
var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1
```
indexOf 底層還是使用 === 進行判斷,因為 NaN === NaN 的結果為 false,所以使用 indexOf 查找不到 NaN 元素
```js
// demo2
function unique(array) {
return Array.from(new Set(array));
}
console.log(unique([NaN, NaN])) // [NaN]
```
Set 認為盡管 NaN === NaN 為 false,但是這兩個元素是重復的。
## 4.用 reduce 實現 map 方法
```js
// map(function(item, index, arr){})
// 對數組的每個元素執行相應的操作,返回一個新數組,其由由操作后的元素組成;不會修改原數組
// reduce(function(prev, curr, index, arr){}, optional)
// 參數依次為:前一個值,當前值,項的索引,數組對象;
// 函數返回的任何值都會作為第一個參數自動傳給下一項,首次執行的prev就是數組第一個元素,curr是數組的第二個元素;
// 還可以接受一個參數作為歸并的初始值,如果傳入了這個參數,首次執行的prev就是這個值,curr是數組的第一個元素
Array.prototype.map = function (callback) {
let arr = this // this->調用該方法的數組
return arr.reduce((prev, curr, index, arr ) => {
prev.push(callback(curr, index, arr))
return prev
}, []) // 最后返回的是prev,傳入[]則第一輪prev = [], curr= 數組第1個元素
}
let m = [1, 2, 3, 4, 5].map((item, index, arr) => {
return item * item
})
console.log(m) // [1, 4, 9, 16, 25]
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs