Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

JavaScript深入之new的模拟实现 #13

Open
mqyqingfeng opened this issue May 4, 2017 · 160 comments
Open

JavaScript深入之new的模拟实现 #13

mqyqingfeng opened this issue May 4, 2017 · 160 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented May 4, 2017

new

一句话介绍 new:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

也许有点难懂,我们在模拟 new 之前,先看看 new 实现了哪些功能。

举个例子:

// Otaku 御宅族,简称宅
function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

// 因为缺乏锻炼的缘故,身体强度让人担忧
Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

从这个例子中,我们可以看到,实例 person 可以:

  1. 访问到 Otaku 构造函数里的属性
  2. 访问到 Otaku.prototype 中的属性

接下来,我们可以尝试着模拟一下了。

因为 new 是关键字,所以无法像 bind 函数一样直接覆盖,所以我们写一个函数,命名为 objectFactory,来模拟 new 的效果。用的时候是这样的:

function Otaku () {
    ……
}

// 使用 new
var person = new Otaku(……);
// 使用 objectFactory
var person = objectFactory(Otaku, ……)

初步实现

分析:

因为 new 的结果是一个新对象,所以在模拟实现的时候,我们也要建立一个新对象,假设这个对象叫 obj,因为 obj 会具有 Otaku 构造函数里的属性,想想经典继承的例子,我们可以使用 Otaku.apply(obj, arguments)来给 obj 添加新的属性。

在 JavaScript 深入系列第一篇中,我们便讲了原型与原型链,我们知道实例的 __proto__ 属性会指向构造函数的 prototype,也正是因为建立起这样的关系,实例可以访问原型上的属性。

现在,我们可以尝试着写第一版了:

// 第一版代码
function objectFactory() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    Constructor.apply(obj, arguments);

    return obj;

};

在这一版中,我们:

  1. 用new Object() 的方式新建了一个对象 obj
  2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  5. 返回 obj

更多关于:

原型与原型链,可以看《JavaScript深入之从原型到原型链》

apply,可以看《JavaScript深入之call和apply的模拟实现》

经典继承,可以看《JavaScript深入之继承》

复制以下的代码,到浏览器中,我们可以做一下测试:

function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    Constructor.apply(obj, arguments);
    return obj;
};

var person = objectFactory(Otaku, 'Kevin', '18')

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName(); // I am Kevin

[]~( ̄▽ ̄)~**

返回值效果实现

接下来我们再来看一种情况,假如构造函数有返回值,举个例子:

function Otaku (name, age) {
    this.strength = 60;
    this.age = age;

    return {
        name: name,
        habit: 'Games'
    }
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // undefined
console.log(person.age) // undefined

在这个例子中,构造函数返回了一个对象,在实例 person 中只能访问返回的对象中的属性。

而且还要注意一点,在这里我们是返回了一个对象,假如我们只是返回一个基本类型的值呢?

再举个例子:

function Otaku (name, age) {
    this.strength = 60;
    this.age = age;

    return 'handsome boy';
}

var person = new Otaku('Kevin', '18');

console.log(person.name) // undefined
console.log(person.habit) // undefined
console.log(person.strength) // 60
console.log(person.age) // 18

结果完全颠倒过来,这次尽管有返回值,但是相当于没有返回值进行处理。

所以我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。

再来看第二版的代码,也是最后一版的代码:

// 第二版的代码
function objectFactory() {

    var obj = new Object(),

    Constructor = [].shift.call(arguments);

    obj.__proto__ = Constructor.prototype;

    var ret = Constructor.apply(obj, arguments);

    return typeof ret === 'object' ? ret : obj;

};

下一篇文章

JavaScript深入之类数组对象与arguments

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入之继承》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@strongcode9527
Copy link

var obj = Object.create(null)
这样创建会比较好

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented May 5, 2017

并没有什么区别吧,毕竟最后都是要更改原型的


以上是最初的看法,两者有很大的不同,欢迎下拉看两者之间的区别。

@jawil
Copy link

jawil commented May 9, 2017

function objectFactory() {

    var obj = new Object(),//从Object.prototype上克隆一个对象

    Constructor = [].shift.call(arguments);//取得外部传入的构造器

    var F=function(){};
    F.prototype= Constructor.prototype;
    obj=new F();//指向正确的原型

    var ret = Constructor.apply(obj, arguments);//借用外部传入的构造器给obj设置属性

    return typeof ret === 'object' ? ret : obj;//确保构造器总是返回一个对象

};

学习学习,之前一直是理论了解new,也来实践模拟一把😄

@mqyqingfeng
Copy link
Owner Author

哈哈,添加的注释很赞 o( ̄▽ ̄)d

@jawil
Copy link

jawil commented May 9, 2017

想看看博主写的react系列,准备学习react了,最近react和vue一直在撕逼😄

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented May 9, 2017

我也好想写React呐,不过React估计是第三个或者第四个系列,现在第一个系列还没有发布完呢,第二个系列估计要写20篇左右,写到React,估计都下下个月了……看来是赶不上这波撕逼的浪潮了~😂😂😂


实际上,过了一年都没有开始写 React 系列 T^T

@wcflmy
Copy link

wcflmy commented May 27, 2017

objectFactory函数里最后一行建议改成“return typeof ret === 'object' ? ret||obj : obj;”,否则如果在Otaku函数里面return null,会有问题的。

@mqyqingfeng
Copy link
Owner Author

@wcflmy 哈哈,被你发现了,我在模拟的时候,也发现这一点了,后来觉得反正主要目的是为了让大家了解 new 的原理,没有必要再写一句专门判断 null ,就没有写,不过使用你这种写法就不用多写那一句了,真的很赞!给你 32 个赞,哈哈~~~ o( ̄▽ ̄)d

@Allen3039
Copy link

Allen3039 commented Jun 1, 2017

return Object.prototype.toString.call(ret).match(/^\[object (\w+)\]$/)[1]==='Object' ? ret : obj; 也可以吧

@mqyqingfeng
Copy link
Owner Author

@Allen3039 哈哈,类型判断加正则,大家这是各显神通呐~ o( ̄▽ ̄)d

@xdwxls
Copy link

xdwxls commented Jun 3, 2017

@jawil var F=function(){};
F.prototype= Constructor.prototype;
obj=new F();//指向正确的原型 这写法是什么意思 没太看明白

@mqyqingfeng
Copy link
Owner Author

@xdwxls 其实就是将 obj 的原型指向 Constructor.prototype,只不过@jawil 的写法中在 new 的模拟实现中又用到了 new 😂

@a1029563229
Copy link

@strongcode9527 试了你这种写法 后面直接用prototype添加的方法,无法被继承....

@mqyqingfeng
Copy link
Owner Author

@a1029563229 能提供下这段代码吗?

@Izayoih
Copy link

Izayoih commented Jul 15, 2017

@mqyqingfeng @a1029563229 我也发现了这个问题 试了一下改成Object.create(Object.prototype)结果可以了...不是很明白什么道理

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Jul 19, 2017

@strongcode9527 @a1029563229 @lzayoih 我也发现了这个问题,测试 demo 为:

function Otaku (age) {}

Otaku.prototype.sayHello = function() {
	console.log('hello')
}

var person = objectFactory(Otaku, 'Kevin', '18');
console.log(person)
person.sayHello() //???

如果使用 Object.create(null),person.sayHello 就会报错,使用 new Object(),会正常打印 hello。

查看由两种方式生成的 person 对象,第一个是由 Object.create 生成的,第二个是 new Object 生成的

default

两者的区别就在于 __proto__ 一个是实的,一个是虚的,由此我们可以猜测第一种方式是把 __proto__ 当成了一个属性值,而非去修改了原型!

原因其实在 《JavaScript深入之从原型到原型链》中有提过:

__proto__ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。

default

@yh284914425
Copy link

__proto__在ie里面不是没用么,代码在ie里面应该实现不了吧

@mqyqingfeng
Copy link
Owner Author

@yh284914425 是的,本篇是希望通过模拟 new 的实现,让大家了解 new 的原理~

@Williris
Copy link

Williris commented Aug 6, 2017

返回值应该判断三种类型: Function,Object 和Array

@qianL93
Copy link

qianL93 commented Aug 31, 2017

这里构造函数返回null的时候,new返回的是obj,不是ret

@mqyqingfeng
Copy link
Owner Author

@qianL93 确实如此哈,@wcflmy@Allen3039 已经在这个 issue 下提出了解决方案~

@huangmxsysu
Copy link

话说__proto__一个实一个虚的问题不是因为Object.create(null)是原型链顶端导致var obj = Object.create(null)之后 obj根本访问不到__proto__这个原型属性导致后面的obj.proto = Constructor.prototype使它的__proto__是实的,成为一个属性,从而没有修改原型。
我觉得是不是可以不需要

var obj = new Object()
obj.__proto__ = Constructor.prototype

直接用

var obj = Object.create(Constructor.prototype)

是一样也可以的吧?

@trayvon2017
Copy link

@strongcode9527 @a1029563229 @lzayoih 我也发现了这个问题,测试 demo 为:

function Otaku (age) {}

Otaku.prototype.sayHello = function() {
	console.log('hello')
}

var person = objectFactory(Otaku, 'Kevin', '18');
console.log(person)
person.sayHello() //???

如果使用 Object.create(null),person.sayHello 就会报错,使用 new Object(),会正常打印 hello。

查看由两种方式生成的 person 对象,第一个是由 Object.create 生成的,第二个是 new Object 生成的

default

两者的区别就在于 proto 一个是实的,一个是虚的,由此我们可以猜测第一种方式是把 proto 当成了一个属性值,而非去修改了原型!

原因其实在 《JavaScript深入之从原型到原型链》中有提过:

proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

default

原型链的顶端是null, 但是null能干什么呢? 原型链能正常工作是内置Object起到的作用,即使Object的prototype是null(仅仅是设计如此).使用Object.create(null)相当于自己创建了一个prototype为null的'CustomObject',它不具备内置Object的一些特性,所以Object.create(null)创建的对象不支持原型特性, 自然也无法根据原型链查找属性/方法
image

@buddywang
Copy link

求问用模拟的实现去实例化一个Date对象不行是什么原因呢?let a=mocknew(Date); a.getDate()报错

@Ljfy
Copy link

Ljfy commented Oct 24, 2021

我也好想写React呐,不过React估计是第三个或者第四个系列,现在第一个系列还没有发布完呢,第二个系列估计要写20篇左右,写到React,估计都下下个月了……看来是赶不上这波撕逼的浪潮了~😂😂😂

实际上,过了一年都没有开始写 React 系列 T^T

实际上,过了四年都没有开始写 React 系列 T^T

@truejiang
Copy link

第一眼看的时候有点懵逼,主要是没习惯arguments直接的用法,换成ES6的写法会明了很多,感谢大佬提供思路,怎么不继续更新了呢
`function objectFactory(Constructor, ...args) {
var obj = new Object()

// Constructor = [].shift.call(arguments)
Object.setPrototypeOf(obj, Constructor.prototype)
// Object.setPrototypeOf(obj, Constructor.prototype)
// obj.proto = Constructor.prototype
console.log(Constructor)

let ret = Constructor.apply(obj, args)

return typeof ret === 'object' ? ret : obj
}`

@daomingQian
Copy link

都说模拟new 咋还函数内用到了new呢

@daomingQian
Copy link

第二版 返回的是null 也不合适 可以改成 return Object(ret ) === ret ? ret : obj

@18602435705
Copy link

function _new(fn) {
  const prototype = fn.prototype;
  const o = Object.create(prototype);
  const result = prototype.constructor.apply(o, [].slice.call(arguments, 1));
  return typeof result === "object" ? result : o;
}

console.log(
  _new(
    function (age, color) {
      this.age = age;
      this.color = color;
    },
    23,
    "red"
  )
);

@leslie555
Copy link

并没有什么区别吧,毕竟最后都是要更改原型的

以上是最初的看法,两者有很大的不同,欢迎下拉看两者之间的区别。

为什么要模拟new 的实现,内部还要用到new 啦, Object.create(null) 感觉确实是正解

@zach-xing
Copy link

我也好想写React呐,不过React估计是第三个或者第四个系列,现在第一个系列还没有发布完呢,第二个系列估计要写20篇左右,写到React,估计都下下个月了……看来是赶不上这波撕逼的浪潮了~😂😂😂
实际上,过了一年都没有开始写 React 系列 T^T

三年了

三年,你知道我这三年怎么过的吗? react我吃定了,耶稣都留不住它

四年了,建国都下台了。

都 2022 年了,还没有写

@cliYao
Copy link

cliYao commented Feb 8, 2022 via email

@Lanbasara
Copy link

function myNew(constru, ...params) {
  let objTarget = new Object();
  let res = constru.apply(objTarget, params);
  Object.setPrototypeOf(objTarget, constru.prototype);
  return typeof res === 'object' ? res || objTarget : objTarget;
}

@dvlin-dev
Copy link

var ret = Constructor.apply(obj, [...arguments].slice(1)) 应该把构造函数去掉在绑定给obj吧

@hotYan
Copy link

hotYan commented Mar 2, 2022

小记

function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

function objectFactory() {
    var obj = new Object(),//存疑,为什么模拟new里面使用new?
    //var obj = Object.create(null),//报错:TypeError
    //var obj = {},//正常
    //var obj = Object.create({}),//正常
    //var obj = Object.create(Object.prototype),//正常

    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);
    return typeof ret === 'object' ? ret||obj : obj;
};

var person = objectFactory(Otaku, 'Kevin', '18')

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60
person.sayYourName(); // I am Kevin

@ccBreeze
Copy link

// 实现 new
function newFactory(fn, ...args) {
  const obj = Object.create(fn.prototype); // 原型式继承
  const ret = fn.apply(obj, args); // 继承属性
  return typeof ret === "object" ? ret : obj;
}

@jiahui336
Copy link

真不错

@cliYao
Copy link

cliYao commented Jun 24, 2022 via email

@wangqianqian0228
Copy link

const obj = Object.create(Constructor.prototype);
创建一个以Constructor.prototype为原型的对象应该也可以吧~

@lwpersonal
Copy link

箭头函数的情况没有处理

@cliYao
Copy link

cliYao commented Jun 21, 2023 via email

@Chenmin926
Copy link

function myNew(fn, ...args) {
let obj = Object.create(fn.prototype);
let res = fn.apply(obj, args);
return res instanceof Object ? res : obj
}

@ZhongJingBo
Copy link

对于 这段代码,有人知道为什么无法new class呢?
class Age {
name='123'
}
function myNew(context) {
const obj = new Object();
constructor = [].shift.call(arguments);
obj.porto = context.prototype;
const ret = constructor.apply(obj, arguments);
return typeof ret === "object" ? ret : obj;
}
const myFun = myNew(Age);
会有如下报错
Uploading image.png…

@cliYao
Copy link

cliYao commented Sep 18, 2023 via email

@bosens-China
Copy link

function _new (fn, ...args) {
  const obj = Object.create(fn.prototype);
  const result = fn.apply(obj, args);

  return (result && typeof result === 'object') ? result: obj;
}

@cliYao
Copy link

cliYao commented Oct 6, 2023 via email

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests