實現一個 new 操作符
- 創建一個新的空對象
- 使空對象的__proto__指向構造函數的原型(prototype)
- 把this綁定到空對象
- 執行構造函數,為空對象添加屬性
- 判斷函數的返回值是否為對象,如果是對象,就使用構造函數的返回值,否則返回創建的對象
- --如果函數沒有返回對象類型Object(包含Functoin, Array, Date, RegExg, Error),那么new表達式中的函數調用將返回該對象引用。
function myNew(Con, ...args){
let obj = {}
obj.__proto__ = Con.prototype
let result = Con.call(obj, ...args)
return result instanceof Object ? result : obj
}
let lin = myNew(Star,'lin',18)
// 相當於
let lin = new Star('lin',18)
實現 call
call核心:
- 第一個參數為null或者undefined時,默認上下文為全局對象window
- 接下來給 context 創建一個 fn 屬性,並將值設置為需要調用的函數
- 為了避免函數名與上下文(context)的屬性發生沖突,使用Symbol類型
- 調用函數
- 函數執行完成后刪除 context.fn 屬性
- 返回執行結果
Function.prototype.myCall = function (context=window, ...args) {
let fn = Symbol('fn');
context.fn = this; // 這里的this就是需要調用的函數,例子中的 bar(name, age) {}
// 調用函數
let result = context.fn(...args); // 這里用了 擴展運算符,將 數組 轉換成 序列
delete context.fn
return result;
}
// 測試
let foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.myCall(foo, 'black', '18') // black 18 1
實現 apply
- 前部分與call一樣
- 第二個參數可以不傳,但類型必須為數組或者類數組
Function.prototype.myApply = function (context=window, ...args) {
let fn = Symbol('fn');
context.fn = this;
let result = context.fn(args);
delete context.fn
return result;
}
- 注:代碼實現存在缺陷,當第二個參數為類數組時,未作判斷(有興趣可查閱一下如何判斷類數組)
實現 bind
bind()方法:會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之后的一序列參數將會在傳遞的實參前傳入作為它的參數。(來自於 MDN )
- 對於普通函數,綁定this指向
- 對於構造函數,要保證原函數的原型對象上的屬性不能丟失
Function.prototype.myBind = function(context, ...args) {
const self = this;
let bindFn = function() {
self.apply(
// 對於普通函數,綁定this指向
// 當返回的綁定函數作為構造函數被new調用,綁定的上下文指向實例對象
this instanceof bindFn ? this : context,
args.concat(...arguments)); // ...arguments這里是將類數組轉換為數組
}
bindFn.prototype = Object.create(self.prototype);
// 返回一個函數
return bindFn;
}
以下是對實現的分析:
let foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.myBind(foo, 'black', '18')
- 前幾步和之前的實現差不多,就不贅述了
- bind 返回了一個函數,
- 對於函數來說有兩種方式調用,一種是直接調用,一種是通過 new 的方式,
- 我們先來說直接調用的方式
- 對於直接調用來說,這里選擇了 apply 的方式實現,
- 但是對於參數需要注意以下情況:
- 因為 bind 可以實現類似這樣的代碼 f.bind(obj, 1)(2),
- 所以我們需要將兩邊的參數拼接起來,於是就有了這樣的實現 args.concat(...arguments)
【補充】
JavaScript 中 call()、apply()、bind() 的用法
bind VS call/apply
- 一個函數被 call/apply 的時候,會直接調用
- bind 會創建一個新函數
當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之后的一序列參數將會在傳遞的實參前傳入作為它的參數。
context = this instanceof Fn ? this : context; // 判斷執行上下文
實現instanceof
object instanceof constructor
- object某個實例對象, constructor某個構造函數
- instanceof 運算符用來檢測 某個構造函數.prototype 是否存在於參數 某個實例對象 的原型鏈上。
instanceof
function myInstanceof(left, right) {
//基本數據類型直接返回false
if (typeof left !== 'object' || left == null) return false;
//getPrototypeOf是Object對象自帶的一個方法,能夠拿到參數的原型對象
let proto = Object.getPrototypeOf(left);
while (true) {
// 如果查找到盡頭,還沒找到,return false
if (proto == null) return false;
//找到相同的原型對象
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String)); //true
淺拷貝
1. 循環遍歷
let obj2 = {};
// 循環遍歷
for (let k in obj1) {
// k 是屬性名 obj1[k] 屬性值
obj2[k] = obj1[k];
}
2. ES6語法 Object.assign
let obj2 = {};
Object.assign(obj2,obj1);
//補充
Object.assign() // 方法用於將所有可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象。
Object.assign(target, ...sources)
3. ES6語法 擴展運算符
let obj2 = {...obj1};
4. arr.slice()
// 語法: arr.slice(begin, end)
let arr1 = [1, 2, 3, 4]
let arr2 = arr1.slice();
深拷貝
1. 簡易版及問題
JSON.parse(JSON.stringify(obj));
該方法的局限性:
-
無法解決循環引用的問題。舉個例子:
const a = {val:2};a.target = a;
拷貝a會出現系統棧溢出,因為出現了無限遞歸的情況。 -
無法拷貝一些特殊的對象,諸如 RegExp, Date, Set, Map等
-
無法拷貝函數
-
會忽略 undefined/symbol
2. 面試可用版
function deepClone(obj){
if(typeof obj == 'object'){
//初始化返回結果
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// 保證 key 不是原型上的屬性
if (obj.hasOwnProperty(key)) {
// 遞歸調用
result[key] = deepClone(obj[key]);
}
}
}else{
//簡單數據類型 直接 賦值
let result = obj;
}
return result;
}
防抖
所謂防抖,就是指觸發事件后在設定的時間期限內函數只能執行一次,如果在設定的時間期限內又觸發了事件,則會重新計算函數執行時間。
function debounce(fn, delay) {
let timer = null;
return function(...args) {
let context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
timer = null;
}, delay);
}
}
節流-定時器
所謂節流,就是指連續觸發事件但是在設定時間內中只執行一次函數。
function throttle(fn, delay = 100) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null;
}, delay)
}
}
}
節流-時間戳
function throttle(fn, delay = 100) {
let previous = 0
return function(...args) {
let context = this
let now = +new Date()
if(now - previous > delay) {
previous = now
fn.apply(context, args)
}
}
}
雙劍合璧
懶加載
// 獲取所有的圖片標簽
const imgs = document.getElementsByTagName('img')
// 獲取可視區域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num 用於統計當前顯示到了哪一張圖片,避免每次都從第一張圖片開始檢查是否露出
let num = 0,
len = imgs.length
function lazyload() {
for (let i = num; i < len; i++) {
// 用可視區域高度 減去 元素頂部距離可視區域頂部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top
// 如果可視區域高度大於等於元素頂部距離可視區域頂部的高度,說明元素露出
// 在chrome瀏覽器中 正常
if (distance >= 50) {
// 給元素寫入真實的 src,展示圖片
imgs[i].src = imgs[i].getAttribute('data-src')
// 前i張圖片已經加載完畢,下次從第i+1張開始檢查是否露出
num = i + 1
}
}
}
// 監聽scroll事件
window.addEventListener('scroll', lazyload)
當然,最好對 scroll 事件做節流處理,以免頻繁觸發:
window.addEventListener('scroll', throttle(lazyload, 200));