前端手写系列(二)

127 阅读8分钟

手写 Object.create 方法

作用

将参数对象作为一个新创建的空对象的原型,并返回这个空对象。

Object.create(null)与{}的区别

  • 通过字面量的方式定义对象,其原型指向Object.prototype,也就是obj.proto === Object.prototype,同时包含了toString, hasOwnProperty等方法。
  • 通过Object.create(null)创建出来的对象是一个 干净对象 ,除自身属性之外,没有附加其他属性。

使用Object.create(null)的原因

  • 通过Object.create(null)创建出来的对象,没有任何属性,显示No properties 。我们可以将其当成一个干净的Map,自主定义toString, hasOwnProperty等方法,不用担心原型链上的同名方法被覆盖。
  • {}创建的对象,使用for in 遍历对象的时候,会遍历原型链上的属性,带来性能上的损耗。

原型式继承

这种继承方法没有使用严格意义上的构造函数,借助原型可以借助已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(obj){
	function fun(){};
    fun.prototype = obj;
    return new fun();
}
  • 创建一个临时的构造函数
  • 将传入的对象作为这个构造函数的原型
  • 返回这个临时对象的一个新实例

来看下面这段代码:

var person = {
	age:18,
	friends:['gray','amili','adward']
	var anotherPerson = object(person)
	anotherPerson.name = "Greg"
	anotherPerson.friends.push("Rob")
	
	var secondPerson = object(person)
		secondPerson.name = "Linda"
		secondPerson.friends.push("Barbie")
	
	alert(person.friends)  // "'gray','amili','adward',"Rob""Barbie""
}

从前面的代码可以看出,object()对传入其中的对象执行了一次浅拷贝

传入的person对象当中,包括了friends这个引用类型,所以 anotherPerson和secondPerson这两个对象在继承了person之后 会共享同一个friends数组。

var instance1 = Object.create(person)
var secondPerson = Object.create(person)
instance1.friend.push('aaa')
secondPerson.friend.push('bbb')
console.log(secondPerson.friend)
//[ 'gray', 'amili', 'adward', 'aaa', 'bbb' ]

Object.create()函数中也是进行了浅拷贝。

代码

function object_create(obj){
	function fun(){};
    fun.prototype = obj;
    return new fun();
}

在传入一个对象的时候,Object.create()与上述object方法是完全一样的。

手写 浅拷贝、深拷贝

数据类型

  • 基本数据类型 直接存储在栈中的数据。
  • 引用数据类型 存储的是该对象在栈中的引用,真实的数据存放在堆内存里。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中地址,取得地址后从堆中获得实体。

作用

深拷贝和浅拷贝只针对Object和Array这样的引用数据类型的。

  • 浅拷贝 只复制指向某个对象的指针,不复制对象本身,新旧对象共享同一块内存。
  • 深拷贝 会另外创造一个一模一样的对象,新对象和原对象不共享内存,修改新对象不会改到原对象。

浅拷贝

  • Object.assign() Object.assign()方法可以把任意多个的原对象自身的可每句属性拷贝给目标对象,然后返回目标对象。
var obj = { a: {a: "kobe", b: 39}, b: "haha" };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.b);	// haha
console.log(obj.a.a); // wade
  • Array.prototype.concat() Array.prototype.concat()方法用于连接两个或多个数组。该方法不会改变现有数组,而仅仅会返回连接数组的一个副本。
let arr = [1, 3, {
   username: 'kobe'
}];
let arr2=arr.concat();    
arr2[2].username = 'wade';
console.log(arr[2].username);	// wade
  • Array.prototype.slice() arr.slice(start, end)函数返回一个数组的一段。该方法也不会改变现有数组,仅仅会返回截取数组的一个副本。
let arr = [1, 3, {
   username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr[2].username);	// wade

深拷贝

  • JSON.parse(JSON.stringify())

原理:JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,这样子新的对象会开辟新的栈,实现深拷贝。

let arr = [1, 3, {
   username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr[2].username, arr4[2].username);
// kobe, duncan

但是这种方法只能实现数组和对象的深拷贝,不能处理函数。因为JSON.stringify方法是将一个js值转换为JSON字符串,不能接收函数。

let arr = [1, 3, {
   username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr[3], arr4[3]);
// 函数f(), null 
  • 手写递归方法

原理:遍历对象、数组知道里面都是基本数据类型,然后再进行复制,实现深度拷贝。

// 定义检测数据类型的功能函数
function checkedType(target) {
 console.log(Object.prototype.toString.call(target))
  return Object.prototype.toString.call(target).slice(8, -1)
}
// 实现深度克隆---对象/数组
function clone(target) {
  // 判断拷贝的数据类型
  // 初始化变量result 成为最终克隆的数据
  let result, targetType = checkedType(target)
  if (targetType === 'Object') {
    result = {}
  } else if (targetType === 'Array') {
    result = []
  } else {
    return target
  }
  // 遍历目标数据
  for (let i in target) {
    // 获取遍历数据结构的每一项值。
    let value = target[i]
    // 判断目标结构里的每一值是否存在对象/数组
    if (checkedType(value) === 'Object' ||
      checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组
      // 继续遍历获取到value值
      result[i] = clone(value)
    } else { 
     // 获取到value值是基本的数据类型或者是函数。
      result[i] = value;
    }
  }
  return result
}

参考

www.jianshu.com/p/35d69cf24…

手写 call / apply / bind

作用

call、apply和bind是为了改变函数运行时的上下文,也就是this指向而存在的。

联系与区别

  • 三者接收的第一个参数都是要绑定的 this指向。
  • call和bind的第二个参数以及之后的参数作为函数实参按顺序传入,apply第二个参数是一个参数数组。
  • bind不会立即调用,会返回一个函数;其他两个会立即调用。

call / apply 代码

Function.prototype.call = function(context) {
  // ctx:需要绑定的上下文,如果没有就绑定window
  const cxt = context || window;
  // 获取参数列表,也可以在形参上使用扩展运算符
  const args = Array.from(arguments).slice(1);
  // 以对象调用的形式调用func,此时this指向ctx,也就是传入的需要绑定的this指向
  const res = arguments.length > 1? cxt.func(...args):cxt.func();
  // 用完后删除该方法,不然会对传入的对象造成污染
  delete cxt.func;
  // 返回调用结果
  return res;
}
Function.prototype.apply = function(context) {
  // ctx:需要绑定的上下文,如果没有就绑定window
  const cxt = context || window;
  // 获取参数列表,也可以在形参上使用扩展运算符
  const args = Array.from(arguments).slice(1);
  // 以对象调用的形式调用func,此时this指向ctx,也就是传入的需要绑定的this指向
  const res = arguments[1]? cxt.func(...arguments[1]):cxt.func();
  // 用完后删除该方法,不然会对传入的对象造成污染
  delete cxt.func;
  // 返回调用结果
  return res;
}

bind

bind函数特点:

  • bind是Function原型链中Function.prototype的一个属性,每个函数都可以调用它。
  • bind本身是一个函数名为bind的函数。返回值也是个函数,这个函数名是bound 。
  • bind函数中this指向传递给它的第一个参数
  • 传给bind的除第一个参数外的其余参数和调用bind返回的函数的参数会合并处理。
  • 调用bind后函数名name为bound + 空格 + 调用bind的函数名,如果是匿名函数则为bound + 空格。
  • bind函数形参只有一个,bind调用后返回的函数形参不定。

据此,我们可以先实现一个简易版。

Function.prototype.bind = function(context) {
	if(typeof this !== 'function'){
    	throw new TypeError(this + 'must be a function')
    }
    // 将当前要执行的函数存储起来
    const that = this;
    // 将其他参数合并成一个数组
    const args = [].slice.call(arguments, 1);
    const bound = function(){
    	// 将 bind函数返回的函数 的参数转成数组
    	let boundArgs = [].slice.call(arguments);
        // 改变函数this指向,并把两个函数参数合并。
        // 执行存储的当前需要执行的函数并返回结果
        return that.apply(context, args.concat(boundArgs));
    }
    return bound;
}
var obj = {
    name: 'aaa',
};
function original(a, b){
    console.log('this', this); // original {}
    console.log('typeof this', typeof this); // object
    this.name = b;
    console.log('name', this.name); // 2
    console.log('this', this);  // original {name: 2}
    console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2}

new 对 this 的影响比 bind 优先级要高,使用new之后,this指向以bound为构造函数的实例。

所以new调用的时候,bind的返回值函数bound内部要模拟实现new实现的操作。

Function.prototype.bind = function(context) {
	if(typeof this !== 'function'){
    	throw new TypeError(this + 'must be a function')
    }
    // 将当前要执行的函数存储起来
    const that = this;
    // 将其他参数合并成一个数组
    const args = [].slice.call(arguments, 1);
    const bound = function(){
    	// 将bind函数返回的函数 的参数转成数组
    	const boundArgs = [].slice.call(arguments);
        // 合并两个参数数组
        const finnalArgs = args.concat(boundArgs);
        // 判断new调用的时候
        if(this insatnceof bound){
        //  that可能是ES6的箭头函数,没有prototype,所以就没必要再指向做prototype操作。
          if(that.prototype){
          // 创建一个全新的对象
            function fun(){};
            // 执行原型对象的连接
            fun.prototype = that.protptype;
            // 通过new创建的对象最终都会被`[[Prototype]]`链接到这个函数的`prototype`对象上
            bound.prototype = new fun();
            // 最终就可以实现将bound的原型指向that,也就是bind所绑定的作用域
          }
          let result = that.apply(this, finalArgs);
          
          // 如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`)
          // 那么`new`表达式中的函数调用会自动返回这个新的对象。
          const isObject = typeof result === 'object' && result !== null;
          const isFunction = typeof result === 'function';
          if(isObject || isFunction){
          	return result;
          }
          
          return this;
      	}else{
          // 改变函数this指向,并把两个函数参数合并。
          // 执行存储的当前需要执行的函数并返回结果
          return that.apply(context, finalArgs);
        }  
    }
    return bound;
}

ECMASript 6 引入一个 new.target 属性,当我们使用 new 操作符调用构造函数时,new.target 属性的值为构造函数,否则为 undefined。所以也可以通过判断new.target是否为undefined来替换this instanceof bound

总结

  • bind是Function原型链中的Function.prototype的一个属性,它是一个函数,修改this指向,合并参数传递给原函数,返回值是一个新的函数。
  • bind返回的函数可以通过new调用,但是这时候提供的this的参数会被忽略,指向了new生成的全新的对象,也就是内部会模拟实现new操作,函数内部this指向以bound为构造函数的实例。

参考

segmentfault.com/a/119000001…