手写 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
}
参考
手写 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为构造函数的实例。