《前端口试手记》之常考的源码完成

👇 内容速览 👇

  • 手动完成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

applycall完成相似,只是传入的参数情势是数组情势,而不是逗号分开的参数序列。

因而,借助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

更多系列文章

⭐在GitHub上珍藏/定阅⭐

《前端学问系统》

《设想形式手册》

《Webpack4渐进式教程》

⭐在GitHub上珍藏/定阅⭐

    原文作者:心谭
    原文地址: https://segmentfault.com/a/1190000018593507
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞