web前端高级 - 深挖new的实现原理并封装自己的new以及实现内置类原型方法的扩展
深挖new的实现原理
在面向对象开发中,我们常常用new关键字来创建一个对象的实例,然后通过这个实例,我们就可以访问类的各种成员了。这是我们对new的最直观的理解。那么在底层我们看不到的地方,new还干了什么呢,接下来我们用代码来做进一步分析。
function Fun(name){
this.name = name;
this.sayHello = function(){
console.log(`hello ${this.name}`);
}
console.log(this);
}
Fun.prototype.sayHi = function(){
}
var fun = new Fun('Alvin');
console.log(fun);
fun.sayHello();
console.log(Fun.prototype);
console.log(fun.__proto__);
console.log(Fun.prototype === fun.__proto__)
上面代码中我们定义了一个构造函数Fun,并在构造函数中定义了一个name属性和一个sayHello方法,然后打印this
在构造函数外面,我们又给原型属性添加了一个sayHi方法
最后创建构造函数的实例,并分别打印实例对象、构造函数原型属性和对象原型。
运行结果如下图所示:
结果分析:
- 1、打印Fun的实例fun输出的是一个对象,则说明:new Fun()的返回值是一个对象
- 2、在new Fun(‘Alvin’)时,输出了this的值(说明console.log(this)被执行了)并且跟打印实例fun的结果是一样的(说明this指向了fun对象);当我们调用sayHello()方法时又输出了“Hello Alvin”(说明this.name=name被执行了);通过上面两点则说明 函数Fun被执行了并且this指向了实例对象fun。
- 3、接着我们又打印了构造函数Fun的原型属性和实例对象的__proto__属性,发现它们输出了同样的结果,而最后一句输出了true则更加印证了二者是相等了。所以这说明:实例对象的__proto__属性指向了Fun的原型属性prototype
综合上面分析得知new的实现原理:
- 1、创建一个对象
- 2、对象有个属性__proto__,并且该属性指向构造函数的原型属性prototype
- 3、执行构造函数并让this指向这个对象
- 4、返回值是一个对象类型
- 如果构造函数没有返回值或返回值是一个基本类型,则将第一步创建的对象返回
- 如果构造函数的返回值是一个引用类型,则返回该引用类型
封装自己的new函数
根据上面的分析及得出的结论,我们来封装一个自己的new函数
function _new(Ctro, ...params){
//1、创建一个对象
let obj = {}
//2、对象有个属性__proto__,并且该属性指向构造函数的原型属性prototype
obj.__proto__ = Ctro.prototype
//3、构造函数执行并让this指向这个对象
let result = Ctro.call(obj,...params)
//5、返回值是一个对象类型
// 如果构造函数没有返回值或返回值是一个基本类型,则将第一步创建的对象返回
if (typeof result === 'object' || typeof result === 'function'){
return result;
}
//如果构造函数的返回值是一个引用类型,则返回该引用类型
return obj;
}
//将上面的代码
var fun = new Fun('Alvin');
//改为我们自己封装的_new函数
var fun = _new(Fun,'Alvin');
再次运行代码输出了同样的结果,这就说明我们封装的_new成功了。但是有个问题我们这里用到的基本都是ES6的新语法,那么一些老的浏览器可能不兼容,那么为了能够兼容所有浏览器,我们要对上面的方法进行改造。这里需要引进一个新的知识点:
- Object.create([obj]),该方法会创建一个空对象,并把[obj]作为当前创建的空对象的__proto__的指向(也就是说当前创建的空对象的__proto__的值是obj)
- [obj]可以是null或是一个对象,如果obj为null,则新创建的空对象不具备__proto__属性,也就是不属于任何类的实例
function _new(Ctro){
//1、创建一个对象
//2、对象有个属性__proto__,并且该属性指向构造函数的原型属性prototype
var obj = Object.create(Ctro.prototype);
//3、构造函数执行并让this指向这个对象
//这里不能用...传递参数,由于参数个数不固定,我们需要用arguments来处理
var params = [].slice.call(arguments, 1);//获取除第一个参数以外的其它参数,以数组的形式保存到params中
//基于apply可以改变this指向也可以把参数以数组的形式传递
var result = Ctro.apply(obj, params)
//5、返回值是一个对象类型
// 如果构造函数没有返回值或返回值是一个基本类型,则将第一步创建的对象返回
if (typeof result === 'object' || typeof result === 'function'){
return result;
}
//如果构造函数的返回值是一个引用类型,则返回该引用类型
return obj;
}
//Object.create()我们还可以重写为:(一般不用这种方式,应为这里也用到了原生的new)
Object.create = function(obj){
function Proxy(){}
Proxy.prototype = obj;
return new Proxy();
}
数组原型方法的扩展
我们都知道,JavaScript中的数组有很多方法,用起来也是非常方便,比如:push、unshift、pop、shift、sort、splice、slice、concat等等,但是这么多方法中却没有去重的,有时候项目中可能需要用到数组去重的场景,这时就不得不需要自己写方法了,而不能像自带的方法那样直接调用。数组去重最简单的方法就是用Set。接下来看下面的代码:
function unique(arr){
let newArr = new Set(arr);
//这里需要注意的是:new Set返回的并不是数组类型而我们处理完成后拿到的结果应该也是一个数组,所以这里需要用Array.from()把结果转换为数组
newArr = Array.from(newArr);
return newArr;
}
以上就实现了数组的去重,但是这样有些太麻烦了,每次都需要自己写方法调用。那么有没有什么办法让去重方法也跟数组内置的方法那样直接调用呢?答案是肯定的,接下来我们就用原型扩展的方式来实现一下:
Array.prototype.unique = function(){
//当我们用一个数组去调用这个unique方法时,那么这里面的this一定是指向该数组的
let uniqueArr = Array.from(new Set(this));
return uniqueArr;
}
如此便实现了内之类原型方法的扩展。
本文地址:https://blog.csdn.net/lixiaosenlin/article/details/109598068