面试官!你看看我手写的new有没有问题
前言
- 大家说我不写技术文章,都是励志文章,那就来重学一波基础吧!看看以现在的眼光学基础能够碰撞出什么火花,文章比较长,涉及到的点都是经过查证的,感谢点赞!读完文章你会发现手写new 是如此简单
从概念到实践
mdn对new的解析
- 概念:new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例
- 这个概念让我有点懵逼,啥意识,有点晦涩难懂,实例是个啥?
???问题 1
:对象类型的实例是个什么玩意????问题 2
:构造函数的内置对象的实例又是啥?
- 语法:new constructor[([arguments])]
- constructor 一个指定对象实例的类型的类或函数。
- arguments 一个用于被
constructor
调用的参数列表
new 做了哪些事
创建一个空的简单JavaScript对象(即{});
为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
将步骤1新创建的对象作为this的上下文 ;
如果该函数没有返回对象,则返回this。
???问题 3
: 为步骤1新创建的对象添加属性__proto__ 不是很懂,添加__proto__ 的用处在哪里,__proto__这个又是什么个玩意????问题 4
: 将该属性链接至构造函数的原型对象 这个操作的意义在哪里????问题 5
: 将步骤1新创建的对象作为this的上下文 是不是指的是this指向新创建的对象?
我是不是废了?这么多问题不懂?
- 是概念太晦涩,还是我基础太差?
上代码 (测试浏览器为谷歌浏览器)
- 看看我们怎么使用new 的
function TestNew(name,age) {
this.name = name;
this.age = age
this.sayHi = function () {
alert('hi')
}
}
const testA = new TestNew('月哥',18)
console.log(testA.name) // 控制台输出 月哥
console.log(testA.name) // 控制台输出 18
testA.sayHi() // 控制台输出
- 我创建了一个函数(为啥要大写),new 的话产生了一个什么东西,我把它赋值给了testA,我来看看testA 是个啥,看下图,是一个对象类型,同时它还可以访问到函数TestNew里面的属性和方法
mdn上说这个函数叫做构造函数,啥叫构造函数,这不就是普通的函数吗?这里就不卖官子了,在js中首字母大写的函数,就是构造函数,这是一个约定,其实作用和小写的完全一致,只是做一个区分
我仿佛明白了概念了,尝试解答一下问题 1 new 运算符创建一个用户定义的对象类型的实例 new 创建了一个对象类型的实例,TestNew是我自己定义的,产生的东西可以理解成为我自己定义的对象类型的实例,艹,问题又来了,实例是一个啥子
实例:让我想到了类的实例化,但是那是java这种强类型语言的,js是弱类型语言的,但是我们也常说,实例化构造函数;有点眉目了,实例就是实际的例子,js中就是指实际的对象,实例化的过程就是创建对象的过程,对象类型的实例,反推一下就是,实例就是对象
但是后面这段什么意识:或具有构造函数的内置对象的实例这个还没有搞清楚?
- 不懂的在构造函数的内置对象是啥?查阅一下文档指的是 Array,Function之类的
- 恍然大悟,原来是这样,new Array(); oh shit!
- 也就是可以这样理解了,自定义的是:new TestNew(),内置的是new Array()之类的,在语法上也解释通了,new constructor[([arguments])] 中的constructor就是一个类(es6)或者函数,可以是内置的,或者自定义的!
总结一下:new 创建了一个对象(实例),对象里面有构造函数里面的属性和方法;伪代码
// 自定义的
testA = {
name: '月哥',
age: 18,
sayHi: function() {
console.log('hi')
}
}
// 构造函数内置的 new Array() 为例子 const testA = new Array()
testA = {
at: function() {},
concat: function() {},
every: function() {},
...
}
- 问题又来了?构造函数中的this.name 之类的为啥要这样写,按道理来说,这个不是挂在全局作用域里面了吗? 难受啊!怎么这么多问题!这个是???问题 6了
继续解决问题
- 刚刚我们解答了问题1,问题2,现在来解答问题3-5,这些问题都是new做了哪些事产生的,
- 问题 3 __proto__这个是什么玩意,给对象添加这个属性有什么作用,我们带着问题去寻找答案!
- proto 我查了一些书籍。类似红宝书,当调用构造函数创建一个新的实例后,该实例的内部将会包含一个指针(内部属性),指向构造函数的原型对象,ECMA-262第五版中管这个指针叫[[prototype]],虽然在脚本中没有明确的方式访问[[prototype]],但是在fx,sf,chrome在每个对象上都支持一个属性__proto__;
- 抄书真累,红宝书对上面__proto__的解析很明确,感觉很高端的样子,总结一下:也就是每个对象实例创建的时候都会有一个内部属性__proto__,指向这个函数的原型对象,__proto__会有浏览器兼容性问题,同时在mdn上被标记已经废弃,大家不要在生产环境上使用! 看下面的图片,每个对象上面都有__proto__属性
- 问题 4: 将该属性链接至构造函数的原型对象 这个操作的意义在哪里?
// 伪代码
// 创建一个新的对象
const newObj = new Object()
// 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
newObj.__proto__ = TestNew.prototype- 查阅文档得知:这段代码的意识是,构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。但是这样做this指向新的对象,指向它干嘛?指向它就是把这个构造函数的作用域赋给新对象,此时这个对象还没有构造函数里面的属性和方法,这一步的操作就是将this指向这个新的对象,为接下来的操作铺路,单独看这段代码确实让很多同学有点闷逼!
- 我们把代码拉出来就是:
- 好我们接着看,算是勉强解释了问题4,同时把问题 5,也解释了,脱离上下文解析感觉就有点强行解释一波了,还是不够充分,
- 最后一步就是返回这个新的对象,ok ,我们来观摩网上各位大佬实现new的代码,
function _new(ctor, ...args) {
if(typeof ctor !== 'function') {
throw 'ctor must be a function';
}
let obj = new Object();
obj.__proto__ = Object.create(TestNew.prototype);
let res = ctor.apply(obj, [...args]);
let isObject = typeof res === 'object' && res !== nu ll;
let isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
};- 这里还有隐藏的操作,如果函数直接return出来了值,结果又不一样了,先记下来,等我们测试代码的时候讲!
- 问题 c:的解答同时也解答了问题 6
- 其他实现中有同学用Object.setPrototypeOf;MDN上不建议用,因为性能比较差。推荐用Object.create()创建对象
- 但是到这里可以解释一下问题 6了, 通过apply这个操作,强行把函数内部的this指向创建的新的对象,从而让我们思考,为什么在一般的函数里面很少能够看到里面写this.name = name之类的东西,因为这种操作是为了配合new 做使用;如果单独写,单独调用的话,这个人绝对要脑子不太好使(手动滑稽一波,哈哈)
代码写的不错,我测试了下,和new的功能基本一致但是我有来找茬了,我有几个问题:
问题 a: 你是实现new这个操作,为何里面还写new Object(),这波操作有点骚啊!
问题 b: obj.proto = Object.create(TestNew.prototype);在这个代码中,为什么要用object.create(),直接写TestNew.prototype不行吗?
问题 c: let res = ctor.apply(obj, [...args]);这个操作是为了执行构造函数的代码,为这个新的对象添加属性和方法,这样写,我觉得很多同学有点看不懂了,为什么这样写就能够给新的对象添加属性和方法呢?
最后一个判断OK,我能看懂;总算是找回了一点信心了!接着往下看
问题 a :这一波操作我觉得挺有争议的,我觉得你不应该在实现new 里面使用new,那你还实现个锤锤!那我们直接换成 let obj = {},这样是不是更加的合适些,先不要管有没有问题,强行先把第一个问题解决了!
问题 b : Object.create()是做啥的,方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__;返回值:一个新对象,带着指定的原型对象和属性。详情请看mdn object.create()链接or (红宝书第170页)但是看了一圈,用这个操作具体的好处是啥(在当前代码中),难道就是使用现有的对象来提供新创建的对象的__proto__ 这个吗?难道直接用TestNew.prototype不行吗?问题还在继续,没有做有说服力的解答
问题 c:let res = ctor.apply(obj, [...args]),使用apply的作用就是为了了让构造函数内部的this指向obj,当指向obj的时候,this.name 等同与 obj.name,这样就非常清晰了,后面的参数传到函数中,正常执行就好,比方ctor === TextNew ,TextNew('月哥',18) 就等同于 obj.name = '月哥',obj.age = 18;
- 第一版
深入聊一下问题 b
- 基于上面的解析我们来实现第二版
- 第二版
function _new1(ctor, ...args) {
if(typeof ctor !== 'function') {
throw 'ctor must be a function';
}
// let obj = new Object();
let obj = {};
// obj.__proto__ = Object.create(ctor.prototype);
obj.__proto__ = ctor.prototype
let res = ctor.apply(obj, [...args]);
let isObject = typeof res === 'object' && res !== nu ll;
let isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
};
- 开始测试
测试环境:google浏览器:版本 100.0.4896.88(正式版本)
测试数据:name: '月哥',age: 18 ,sayHi(),原型上的sayGood()
测试范围:new, _new(),_new1()
测试用例:输出 实例对象,输出 name ,输出age,调用sayHi方法
预期结果:_new() & _new1(),表现形式与new 保持一致
测试代码:
function TestNew(name,age) {
this.name = name;
this.age = age
this.sayHi = function () {
console.log('hi')
}
}
TestNew.prototype.sayGood = function () {
console.log('你好!点个赞再走!')
}
function _new(ctor, ...args) {
if(typeof ctor !== 'function') {
throw 'ctor must be a function';
}
let obj = new Object();
obj.__proto__ = Object.create(TestNew.prototype);
let res = ctor.apply(obj, [...args]);
let isObject = typeof res === 'object' && res !== null;
let isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
};
function _new1(ctor, ...args) {
if(typeof ctor !== 'function') {
throw 'ctor must be a function';
}
let obj = {};
obj.__proto__ = ctor.prototype
let res = ctor.apply(obj, [...args]);
let isObject = typeof res === 'object' && res !== null;
let isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
};
const testNew = new TestNew('月哥',18)
const test_New = _new(TestNew,'月哥',18)
const test_New1 = _new1(TestNew,'月哥',18)
console.log('---使用new测试结果----')
console.log(testNew)
console.log(testNew.age)
console.log(testNew.name)
testNew.sayHi();
testNew.sayGood()
console.log('---使用_new测试结果----')
console.log(test_New)
console.log(test_New.age)
console.log(test_New.name)
testNew.sayHi();
testNew.sayGood()
console.log('---使用_new1测试结果----')
console.log(test_New1)
console.log(test_New1.age)
console.log(test_New1.name)
test_New1.sayHi();
test_New1.sayGood()
- 测试结果:
- 细心的同学发了问题了吗???
- 不测不知道 ,使用:obj.proto = Object.create(TestNew.prototype)的_new 实例打印出来是两层的[[Prototype]];
- new,和_new1表现形式一致,也就意味着在表现形式上,使用object.create()看起来是有那么问题的;
- so这样 obj = Object.create(TestNew.prototype)
- 问题的原因是多了一层的原因是因为 Object.create(TestNew.prototype) 是基于目标对象TestNew.prototype创建出一个实例,概念上使用现有的对象来提供新创建的对象的__proto__([[Prototype]] 等价__proto__,有的浏览器会是两层__proto__)
结论来了,可以不用Object.create()来指定原型,这样的表现形式会多一层__proto__
好处没发现,坏处发现了,我们一般常用,Object.create(null)用来创建一个纯净的对象,直接上代码看一下,很清晰!
边界测试
- 修改TestNew()
function TestNew(name,age) {
this.name = name;
this.age = age
this.sayHi = function () {
alert('hi')
}
return null
// return 1
// return '1'
// return true
// return {}
// return function(){}
// return /a/
}
- 结果基础数据类型直接正常返回值,引用数据类型,直接返回函数内返回的值,同时,函数上的方法报错,其他截图就不贴了,大家可以自己看下
总结
- 上面说了这么多其实就一句话:new的核心作用:创建产生一个新的实例对象,这个实例对象拥有函数的属性和方法
- 面试中你直接可以这样说了,你们可能问为啥这么短,new的细节这么多,而且为啥不说mdn上对new的概念,但是你要记住,你要形成你自己的思考,你自己的沉淀(哈哈,满满的阿里味,细细想来,好像说的也挺对!)很多时候你说的多不一定说的对,一定要抓住重点,而且中华文化博大精深,我说了核心作用,话是没有说死了,如果面试官继续追问,直接就可以顺着话往下面说,什么原型啥的,我都没有点,但是你问我可以讲的很明白,要是不熟,不要给你自己挖坑!