您好,登錄后才能下訂單哦!
JavaScript中各種源碼的實(shí)現(xiàn)是怎樣的,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來(lái)學(xué)習(xí)下,希望你能有所收獲。
能夠手寫(xiě)實(shí)現(xiàn)各種JavaScript原生函數(shù),可以說(shuō)是擺脫 API調(diào)用師 帽子的第一步,我們不光要會(huì)用,更要去探究其實(shí)現(xiàn)原理!
對(duì)JavaScript源碼的學(xué)習(xí)和實(shí)現(xiàn)能幫助我們快速和扎實(shí)地提升自己的前端編程能力。
實(shí)現(xiàn)一個(gè)new操作符
我們首先知道new做了什么:
創(chuàng)建一個(gè)空的簡(jiǎn)單JavaScript對(duì)象(即{});
鏈接該對(duì)象(即設(shè)置該對(duì)象的構(gòu)造函數(shù))到另一個(gè)對(duì)象 ;
將步驟(1)新創(chuàng)建的對(duì)象作為this的上下文 ;
如果該函數(shù)沒(méi)有返回對(duì)象,則返回this。
知道new做了什么,接下來(lái)我們就來(lái)實(shí)現(xiàn)它
function create(Con, ...args){
// 創(chuàng)建一個(gè)空的對(duì)象
this.obj = {};
// 將空對(duì)象指向構(gòu)造函數(shù)的原型鏈
Object.setPrototypeOf(this.obj, Con.prototype);
// obj綁定到構(gòu)造函數(shù)上,便可以訪問(wèn)構(gòu)造函數(shù)中的屬性,即this.obj.Con(args)
let result = Con.apply(this.obj, args);
// 如果返回的result是一個(gè)對(duì)象則返回
// new方法失效,否則返回obj
return result instanceof Object ? result : this.obj;
}
實(shí)現(xiàn)一個(gè)Array.isArray
思路很簡(jiǎn)單,就是利用Object.prototype.toString
Array.myIsArray = function(o) {
return Object.prototype.toString.call(Object(o)) === '[object Array]';
};
實(shí)現(xiàn)一個(gè)Object.create()方法
function create = function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
實(shí)現(xiàn)一個(gè)EventEmitter
真實(shí)經(jīng)歷,最近在字節(jié)跳動(dòng)的面試中就被面試官問(wèn)到了,讓我手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Event類(lèi)。
class Event {
constructor () {
// 儲(chǔ)存事件的數(shù)據(jù)結(jié)構(gòu)
// 為查找迅速, 使用對(duì)象(字典)
this._cache = {}
}
// 綁定
on(type, callback) {
// 為了按類(lèi)查找方便和節(jié)省空間
// 將同一類(lèi)型事件放到一個(gè)數(shù)組中
// 這里的數(shù)組是隊(duì)列, 遵循先進(jìn)先出
// 即新綁定的事件先觸發(fā)
let fns = (this._cache[type] = this._cache[type] || [])
if(fns.indexOf(callback) === -1) {
fns.push(callback)
}
return this
}
// 解綁
off (type, callback) {
let fns = this._cache[type]
if(Array.isArray(fns)) {
if(callback) {
let index = fns.indexOf(callback)
if(index !== -1) {
fns.splice(index, 1)
}
} else {
// 全部清空
fns.length = 0
}
}
return this
}
// 觸發(fā)emit
trigger(type, data) {
let fns = this._cache[type]
if(Array.isArray(fns)) {
fns.forEach((fn) => {
fn(data)
})
}
return this
}
// 一次性綁定
once(type, callback) {
let wrapFun = () => {
callback.call(this);
this.off(type, callback);
};
this.on(wrapFun, callback);
return this;
}
}
let e = new Event()
e.on('click',function(){
console.log('on')
})
e.on('click',function(){
console.log('onon')
})
// e.trigger('click', '666')
console.log(e)
實(shí)現(xiàn)一個(gè)Array.prototype.reduce
首先觀察一下Array.prototype.reduce語(yǔ)法;
Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
然后就可以動(dòng)手實(shí)現(xiàn)了:
Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue ? initialValue : this[0];
for (let i = initialValue ? 0 : 1; i < this.length; i++) { let _this = this; accumulator = callback(accumulator, this[i], i, _this); } return accumulator; }; // 使用 let arr = [1, 2, 3, 4]; let sum = arr.myReduce((acc, val) => {
acc += val;
return acc;
}, 5);
console.log(sum); // 15
實(shí)現(xiàn)一個(gè)call或apply
先來(lái)看一個(gè)call實(shí)例,看看call到底做了什么:
let foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
從代碼的執(zhí)行結(jié)果,我們可以看到,call首先改變了this的指向,使函數(shù)的this指向了foo,然后使bar函數(shù)執(zhí)行了。
總結(jié)一下:
call改變函數(shù)this指向
調(diào)用函數(shù)
思考一下:我們?nèi)绾螌?shí)現(xiàn)上面的效果呢?代碼改造如下:
Function.prototype.myCall = function(context) {
context = context || window;
//將函數(shù)掛載到對(duì)象的fn屬性上
context.fn = this;
//處理傳入的參數(shù)
const args = [...arguments].slice(1);
//通過(guò)對(duì)象的屬性調(diào)用該方法
const result = context.fn(...args);
//刪除該屬性
delete context.fn;
return result
};
我們看一下上面的代碼:
首先我們對(duì)參數(shù)context做了兼容處理,不傳值,context默認(rèn)值為window;
然后我們將函數(shù)掛載到context上面,context.fn = this;
處理參數(shù),將傳入myCall的參數(shù)截取,去除第一位,然后轉(zhuǎn)為數(shù)組;
調(diào)用context.fn,此時(shí)fn的this指向context;
刪除對(duì)象上的屬性 delete context.fn;
將結(jié)果返回。
以此類(lèi)推,我們順便實(shí)現(xiàn)一下apply,唯一不同的是參數(shù)的處理,代碼如下:
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this
let result
// myApply的參數(shù)形式為(obj,[arg1,arg2,arg3]);
// 所以myApply的第二個(gè)參數(shù)為[arg1,arg2,arg3]
// 這里我們用擴(kuò)展運(yùn)算符來(lái)處理一下參數(shù)的傳入方式
if (arguments[1]) {
result = context.fn(…arguments[1])
} else {
result = context.fn()
}
delete context.fn;
return result
};
以上便是call和apply的模擬實(shí)現(xiàn),唯一不同的是對(duì)參數(shù)的處理方式。
實(shí)現(xiàn)一個(gè)Function.prototype.bind
function Person(){
this.name="zs";
this.age=18;
this.gender="男"
}
let obj={
hobby:"看書(shū)"
}
// 將構(gòu)造函數(shù)的this綁定為obj
let changePerson = Person.bind(obj);
// 直接調(diào)用構(gòu)造函數(shù),函數(shù)會(huì)操作obj對(duì)象,給其添加三個(gè)屬性;
changePerson();
// 1、輸出obj
console.log(obj);
// 用改變了this指向的構(gòu)造函數(shù),new一個(gè)實(shí)例出來(lái)
let p = new changePerson();
// 2、輸出obj
console.log(p);
仔細(xì)觀察上面的代碼,再看輸出結(jié)果。
我們對(duì)Person類(lèi)使用了bind將其this指向obj,得到了changeperson函數(shù),此處如果我們直接調(diào)用changeperson會(huì)改變obj,若用new調(diào)用changeperson會(huì)得到實(shí)例 p,并且其__proto__指向Person,我們發(fā)現(xiàn)bind失效了。
我們得到結(jié)論: 用bind改變了this指向的函數(shù),如果用new操作符來(lái)調(diào)用,bind將會(huì)失效。
這個(gè)對(duì)象就是這個(gè)構(gòu)造函數(shù)的實(shí)例,那么只要在函數(shù)內(nèi)部執(zhí)行 this instanceof 構(gòu)造函數(shù) 來(lái)判斷其結(jié)果是否為true,就能判斷函數(shù)是否是通過(guò)new操作符來(lái)調(diào)用了,若結(jié)果為true則是用new操作符調(diào)用的,代碼修正如下:
// bind實(shí)現(xiàn)
Function.prototype.mybind = function(){
// 1、保存函數(shù)
let _this = this;
// 2、保存目標(biāo)對(duì)象
let context = arguments[0]||window;
// 3、保存目標(biāo)對(duì)象之外的參數(shù),將其轉(zhuǎn)化為數(shù)組;
let rest = Array.prototype.slice.call(arguments,1);
// 4、返回一個(gè)待執(zhí)行的函數(shù)
return function F(){
// 5、將二次傳遞的參數(shù)轉(zhuǎn)化為數(shù)組;
let rest2 = Array.prototype.slice.call(arguments)
if(this instanceof F){
// 6、若是用new操作符調(diào)用,則直接用new 調(diào)用原函數(shù),并用擴(kuò)展運(yùn)算符傳遞參數(shù)
return new _this(...rest2)
}else{
//7、用apply調(diào)用第一步保存的函數(shù),并綁定this,傳遞合并的參數(shù)數(shù)組,即context._this(rest.concat(rest2))
_this.apply(context,rest.concat(rest2));
}
}
};
實(shí)現(xiàn)一個(gè)JS函數(shù)柯里化
Currying的概念其實(shí)并不復(fù)雜,用通俗易懂的話說(shuō):只傳遞給函數(shù)一部分參數(shù)來(lái)調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。
function progressCurrying(fn, args) {
let _this = this
let len = fn.length;
let args = args || [];
return function() {
let _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果參數(shù)個(gè)數(shù)小于最初的fn.length,則遞歸調(diào)用,繼續(xù)收集參數(shù)
if (_args.length < len) { return progressCurrying.call(_this, fn, _args); } // 參數(shù)收集完畢,則執(zhí)行fn return fn.apply(this, _args); } } 手寫(xiě)防抖(Debouncing)和節(jié)流(Throttling) 節(jié)流
防抖函數(shù) onscroll 結(jié)束時(shí)觸發(fā)一次,延遲執(zhí)行
function debounce(func, wait) {
let timeout;
return function() {
let context = this; // 指向全局
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
func.apply(context, args); // context.func(args)
}, wait);
};
}
// 使用
window.onscroll = debounce(function() {
console.log('debounce');
}, 1000);
節(jié)流函數(shù) onscroll 時(shí),每隔一段時(shí)間觸發(fā)一次,像水滴一樣
function throttle(fn, delay) {
let prevTime = Date.now();
return function() {
let curTime = Date.now();
if (curTime - prevTime > delay) {
fn.apply(this, arguments);
prevTime = curTime;
}
};
}
// 使用
var throtteScroll = throttle(function() {
console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;
JSON.parse(JSON.stringfy));
非常簡(jiǎn)單,但缺陷也很明顯,比如拷貝其他引用類(lèi)型、拷貝函數(shù)、循環(huán)引用等情況。
基礎(chǔ)版
function clone(target){
if(typeof target === 'object'){
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = clone(target[key])
}
return cloneTarget;
} else {
return target
}
}
寫(xiě)到這里已經(jīng)可以幫助你應(yīng)付一些面試官考察你的遞歸解決問(wèn)題的能力。但是顯然,這個(gè)深拷貝函數(shù)還是有一些問(wèn)題。
一個(gè)比較完整的深拷貝函數(shù),需要同時(shí)考慮對(duì)象和數(shù)組,考慮循環(huán)引用:
function clone(target, map = new WeakMap()) {
if(typeof target === 'object'){
let cloneTarget = Array.isArray(target) ? [] : {};
if(map.get(target)) {
return target;
}
map.set(target, cloneTarget);
for(const key in target) {
cloneTarget[key] = clone(target[key], map)
}
return cloneTarget;
} else {
return target;
}
}
實(shí)現(xiàn)一個(gè)instanceOf
原理: L 的 proto 是不是等于 R.prototype,不等于再找 L.__proto__.__proto__ 直到 proto 為 null
// L 表示左表達(dá)式,R 表示右表達(dá)式
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null){
return false;
}
// 這里重點(diǎn):當(dāng) O 嚴(yán)格等于 L 時(shí),返回 true
if (O === L) {
return true;
}
L = L.__proto__;
}
}
實(shí)現(xiàn)原型鏈繼承
function myExtend(C, P) {
var F = function(){};
F.prototype = P.prototype;
C.prototype = new F();
C.prototype.constructor = C;
C.super = P.prototype;
}
實(shí)現(xiàn)一個(gè)async/await
原理
就是利用 generator(生成器)分割代碼片段。然后我們使用一個(gè)函數(shù)讓其自迭代,每一個(gè)yield 用 promise 包裹起來(lái)。執(zhí)行下一步的時(shí)機(jī)由 promise 來(lái)控制
實(shí)現(xiàn)
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
// 將返回值promise化
return new Promise(function(resolve, reject) {
// 獲取迭代器實(shí)例
var gen = fn.apply(self, args);
// 執(zhí)行下一步
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
// 拋出異常
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
// 第一次觸發(fā)
_next(undefined);
});
};
}
實(shí)現(xiàn)一個(gè)Array.prototype.flat()函數(shù)
最近字節(jié)跳動(dòng)的前端面試中也被面試官問(wèn)到,要求手寫(xiě)實(shí)現(xiàn)。
Array.prototype.myFlat = function(num = 1) {
if (Array.isArray(this)) {
let arr = [];
if (!Number(num) || Number(num) < 0) { return this; } this.forEach(item => {
if(Array.isArray(item)){
let count = num
arr = arr.concat(item.myFlat(--count))
} else {
arr.push(item)
}
});
return arr;
} else {
throw tihs + ".flat is not a function";
}
};
實(shí)現(xiàn)一個(gè)事件代理
這個(gè)問(wèn)題一般還會(huì)讓你講一講事件冒泡和事件捕獲機(jī)制
red
yellow
blue
green
black
white
實(shí)現(xiàn)一個(gè)雙向綁定
Vue 2.x的Object.defineProperty版本
// 數(shù)據(jù)
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數(shù)據(jù)劫持
Object.defineProperty(data, 'text', {
// 數(shù)據(jù)變化 —> 修改視圖
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 視圖更改 --> 數(shù)據(jù)變化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
Vue 3.x的proxy 版本
// 數(shù)據(jù)
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 數(shù)據(jù)劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 數(shù)據(jù)變化 —> 修改視圖
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 視圖更改 --> 數(shù)據(jù)變化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。