👇 內容速覽 👇
- 手動實現
call
/apply
/bind
- 實現一個完美的深拷貝函數
- 基於
ES5
/ES6
實現雙向綁定 -
instanceof
原理與實現
手動擼個call/apply/bind
實現call
來看下call
的原生表現形式:
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}
run.call({
a: 'a',
b: 'b'
}, 1, 2)
好了,開始手動實現我們的call2
。在實現的過程有個關鍵:
如果一個函數作爲一個對象的屬性,那麼通過對象的.
運算符調用此函數,this
就是此對象
let obj = {
a: 'a',
b: 'b',
test: function (arg1, arg2) {
console.log(arg1, arg2)
// this.a 就是 a; this.b 就是 b
console.log(this.a, this.b)
}
}
obj.test(1, 2)
知道了實現關鍵,下面就是我們模擬的call
:
Function.prototype.call2 = function(context) {
if(typeof this !== 'function') {
throw new TypeError('Error')
}
// 默認上下文是window
context = context || window
// 保存默認的fn
const { fn } = context
// 前面講的關鍵,將函數本身作爲對象context的屬性調用,自動綁定this
context.fn = this
const args = [...arguments].slice(1)
const result = context.fn(...args)
// 恢復默認的fn
context.fn = fn
return result
}
// 以下是測試代碼
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}
test.call2({
a: 'a',
b: 'b'
}, 1, 2)
實現apply
apply
和call
實現類似,只是傳入的參數形式是數組形式,而不是逗號分隔的參數序列。
因此,藉助es6提供的...
運算符,就可以很方便的實現數組和參數序列的轉化。
Function.prototype.apply2 = function(context) {
if(typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
const { fn } = context
context.fn = this
let result
if(Array.isArray(arguments[1])) {
// 通過...運算符將數組轉換爲用逗號分隔的參數序列
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
context.fn = fn
return result
}
/**
* 以下是測試代碼
*/
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}
test.apply2({
a: 'a',
b: 'b'
}, [1, 2])
實現bind
bind
的實現有點意思,它有兩個特點:
- 本身返回一個新的函數,所以要考慮
new
的情況 - 可以“保留”參數,內部實現了參數的拼接
Function.prototype.bind2 = function(context) {
if(typeof this !== 'function') {
throw new TypeError('Error')
}
const that = this
// 保留之前的參數,爲了下面的參數拼接
const args = [...arguments].slice(1)
return function F() {
// 如果被new創建實例,不會被改變上下文!
if(this instanceof F) {
return new that(...args, ...arguments)
}
// args.concat(...arguments): 拼接之前和現在的參數
// 注意:arguments是個類Array的Object, 用解構運算符..., 直接拿值拼接
return that.apply(context, args.concat(...arguments))
}
}
/**
* 以下是測試代碼
*/
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}
const test2 = test.bind2({
a: 'a',
b: 'b'
}, 1) // 參數 1
test2(2) // 參數 2
實現一個完美的深拷貝函數
實現一個對象的深拷貝函數,需要考慮對象的元素類型以及對應的解決方案:
- 基礎類型:這種最簡單,直接賦值即可
- 對象類型:遞歸調用拷貝函數
- 數組類型:這種最難,因爲數組中的元素可能是基礎類型、對象還可能數組,因此要專門做一個函數來處理數組的深拷貝
/**
* 數組的深拷貝函數
* @param {Array} src
* @param {Array} target
*/
function cloneArr(src, target) {
for(let item of src) {
if(Array.isArray(item)) {
target.push(cloneArr(item, []))
} else if (typeof item === 'object') {
target.push(deepClone(item, {}))
} else {
target.push(item)
}
}
return target
}
/**
* 對象的深拷貝實現
* @param {Object} src
* @param {Object} target
* @return {Object}
*/
function deepClone(src, target) {
const keys = Reflect.ownKeys(src)
let value = null
for(let key of keys) {
value = src[key]
if(Array.isArray(value)) {
target[key] = cloneArr(value, [])
} else if (typeof value === 'object') {
// 如果是對象而且不是數組, 那麼遞歸調用深拷貝
target[key] = deepClone(value, {})
} else {
target[key] = value
}
}
return target
}
這段代碼是不是比網上看到的多了很多?因爲考慮很周全,請看下面的測試用例:
// 這個對象a是一個囊括以上所有情況的對象
let a = {
age: 1,
jobs: {
first: "FE"
},
schools: [
{
name: 'shenda'
},
{
name: 'shiyan'
}
],
arr: [
[
{
value: '1'
}
],
[
{
value: '2'
}
],
]
};
let b = {}
deepClone(a, b)
a.jobs.first = 'native'
a.schools[0].name = 'SZU'
a.arr[0][0].value = '100'
console.log(a.jobs.first, b.jobs.first) // output: native FE
console.log(a.schools[0], b.schools[0]) // output: { name: 'SZU' } { name: 'shenda' }
console.log(a.arr[0][0].value, b.arr[0][0].value) // output: 100 1
console.log(Array.isArray(a.arr[0])) // output: true
看到測試用例,應該會有人奇怪爲什麼最後要輸出Array.isArray(a.arr[0])
。這主要是因爲網上很多實現方法沒有針對array做處理,直接將其當成object,這樣拷貝後雖然值沒問題,但是array的元素會被轉化爲object。這顯然是錯誤的做法。
而上面所說的深拷貝函數就解決了這個問題。
基於ES5/ES6實現“雙向綁定”
要想實現,就要先看看什麼是“雙向數據綁定”,它和“單向數據綁定”有什麼區別?這樣才能知道要實現什麼效果嘛。
雙向綁定:視圖(View)的變化能實時讓數據模型(Model)發生變化,而數據的變化也能實時更新到視圖層。
單向數據綁定:只有從數據到視圖這一方向的關係。
ES5的Object.defineProperty
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const obj = {
value: ''
}
function onKeyUp(event) {
obj.value = event.target.value
}
// 對 obj.value 進行攔截
Object.defineProperty(obj, 'value', {
get: function() {
return value
},
set: function(newValue) {
value = newValue
document.querySelector('#value').innerHTML = newValue // 更新視圖層
document.querySelector('input').value = newValue // 數據模型改變
}
})
</script>
</head>
<body>
<p>
值是:<span id="value"></span>
</p>
<input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
ES6的Proxy
隨着,vue3.0放棄支持了IE瀏覽器。而且Proxy
兼容性越來越好,能支持13種劫持操作。
因此,vue3.0選擇使用Proxy
來實現雙向數據綁定,而不再使用Object.defineProperty
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const obj = {}
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set: function(target, key, value, receiver) {
if(key === 'value') {
document.querySelector('#value').innerHTML = value
document.querySelector('input').value = value
}
return Reflect.set(target, key, value, receiver)
}
})
function onKeyUp(event) {
newObj.value = event.target.value
}
</script>
</head>
<body>
<p>
值是:<span id="value"></span>
</p>
<input type="text" onkeyup="onKeyUp(event)">
</body>
</html>
instanceof原理與實現
instanceof
是通過原型鏈來進行判斷的,所以只要不斷地通過訪問__proto__
,就可以拿到構造函數的原型prototype
。直到null
停止。
/**
* 判斷left是不是right類型的對象
* @param {*} left
* @param {*} right
* @return {Boolean}
*/
function instanceof2(left, right) {
let prototype = right.prototype;
// 沿着left的原型鏈, 看看是否有何prototype相等的節點
left = left.__proto__;
while(1) {
if(left === null || left === undefined) {
return false;
}
if(left === prototype) {
return true;
}
left = left.__proto__;
}
}
/**
* 測試代碼
*/
console.log(instanceof2([], Array)) // output: true
function Test(){}
let test = new Test()
console.log(instanceof2(test, Test)) // output: true
更多系列文章
《前端知識體系》
《設計模式手冊》
《Webpack4漸進式教程》