mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。
30.66k stars 4.7k forks source link

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

Open mqyqingfeng opened 7 years ago

mqyqingfeng commented 7 years ago

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 commented 7 years ago

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

mqyqingfeng commented 7 years ago

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


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

jawil commented 7 years ago
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 commented 7 years ago

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

jawil commented 7 years ago

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

mqyqingfeng commented 7 years ago

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


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

wcflmy commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

Allen3039 commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

xdwxls commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

a1029563229 commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

Izayoih commented 7 years ago

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

mqyqingfeng commented 7 years ago

@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 commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

Williris commented 7 years ago

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

qianL93 commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

huangmxsysu commented 7 years ago

话说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)

是一样也可以的吧?

mqyqingfeng commented 7 years ago

@huangmxsysu 可以呀,毕竟 Object.create 的实现方式与之类似,之所以用 __proto__ 的方式是考虑之前的文章并未讲过 Object.create 的方法,使用这个方法还需要多一层解释成本

huangmxsysu commented 7 years ago

嗯嗯,昨天读了你这些文章,收获蛮大的!棒!后面有再写什么么?

mqyqingfeng commented 7 years ago

@huangmxsysu 写完专题系列,接下来会写 ES6 系列 和 React 系列,欢迎关注哈~

JeffAAA commented 6 years ago

在处理返回值的时候,我发现使用new出来的实例的proto和我们模拟出来的实例的proto是不一样的,导致获取不到原型链上对应的值。有点没懂,可否解答一下@mqyqingfeng,代码如下

var objectFactory = function () {
  var obj = new Object();
  var constructor = Array.prototype.shift.call(arguments);
  obj.__proto__ = constructor.prototype;
  var returnValue = constructor.apply(obj, arguments);
  return typeof retrunValue == 'object' ? retrunValue : obj
}

var Person = function (name, age) {
  this.name = name;
  this.age = age;
  return {
    name: name, age: age
  }
}

Person.prototype.height = '160';
Person.prototype.sayHi = function () {
  return this.name + ' say hi!';
}
var newPerson = new Person('jeff', '11');
var factoryPerson = objectFactory(Person, 'jeff', '12');
console.log(newPerson.name, factoryPerson.name) // jeff jeff
console.log(newPerson.age, factoryPerson.age) // 11 12
console.log(newPerson.height, factoryPerson.height) // undefined "160"
console.log(newPerson.sayHi, factoryPerson.sayHi) //  undefined ƒ () {return this.name + ' say hi!';}

那这样,是不是我们没有成功模拟到new一个实例的功能呢

mqyqingfeng commented 6 years ago

@JeffAAA 你这里写错了哈:

var returnValue = constructor.apply(obj, arguments);
return typeof retrunValue == 'object' ? retrunValue : obj

都改成returnValue 结果就统一了~

JeffAAA commented 6 years ago

😅 好吧 手残了 ~~

ghost commented 6 years ago
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;//确保构造器总是返回一个对象

};

@jawil 你第一行代码new出Object实例,然后把该实例赋值给obj变量,但是在第五行又重新给obj变量赋值。所以我感觉第一行赋值为Object实例没啥意义。另外,第一行代码你也可以这样写,var obj = new Object;

RayJune commented 6 years ago

感谢作者 @mqyqingfeng ,学到了很多,也写一个自己优化的版本

function objectFactory() {
  var args = Array.prototype.slice.call(arguments);
  var Constructor = args.shift();
  var instance = Object.create(Constructor.prototype);
  var temp = Constructor.apply(instance, args);

  return (typeof temp === 'object' && temp !== null) ? temp : instance;
}
mqyqingfeng commented 6 years ago

@RayJune 感谢补充哈~

easterCat commented 6 years ago
obj.__proto__ = Constructor.prototype;

这个是不是把function Object(){}的prototype给覆盖了?

mqyqingfeng commented 6 years ago

@a792751238 是的,不过 Constructor.prototype 的 __proto__ 又是这个 Object.prototype……

default
zhouyingkai1 commented 6 years ago

又复习一遍 受益良多 🙏,谢谢

看到博主提到react, 想问下博主有计划写react系列吗,或者什么时候会写

ClarenceC commented 6 years ago
function objectFactory() {
  // 使用objectFactory的时候,把arguments,转化为数组
  var args = Array.prototype.slice.call(arguments);
  //提取第1个构建对象
  var Constructor = args.shift();
  // 创建constructor实例 instance 
  var instance = Object.create(Constructor.prototype);
  // 使用apply函数运行args, 把 instance 绑定到 this
  var temp = Constructor.apply(instance, args); 
  //返回对象判断 是object 还是 null 还是实例
  return (typeof temp === 'object' && temp !== null) ? temp : instance; 
}
mqyqingfeng commented 6 years ago

@ClarenceC 感谢分享哈~

enggirl commented 6 years ago

博主你好,Object.create() vs new Object()区别,我的理解是new Object()得到的实例对象,有proto属性也有constructor属性,但是Object.create()创建的对象没有这两个属性,而且用instanceof 判断是否是Object,用Object.create() 创建的对象也是false。在MDN上面的例子,看了之后觉得,Object.create()是可以设置某个属性的底层是否可操作,但是这一点在new Object()没有。而且我在看到stackoverflow上说Object.create()其实并没有继承什么东西。new Object() 可以返回实际对象 https://stackoverflow.com/questions/4166616/understanding-the-difference-between-object-create-and-new-somefunction/4166723#4166723
不知道这么理解对不对,还望指点迷津

以下是代码:

image

mqyqingfeng commented 6 years ago

@enggirl 我错了,楼下的是正解!

huangmxsysu commented 6 years ago

@enggirl 准确来讲是因为Object.create(null)返回一个没有任务继承关系的空值,你才觉得没有proto或者construtor或者instanceof返回false,当你使用Object.create(Object.prototype)的时候又是另一回事了那些属性

lizhongzhen11 commented 6 years ago

赞同楼上。 试试这个:

var obj1 = new Object();
var obj2 = Object.create(Object.prototype);
obj1.constructor === Object; // true
obj1.__proto__ === Object.prototype; // true
obj2.constructor === Object; // true
obj2.__proto__ === Object.prototype; // true

@enggirl

Latube commented 6 years ago

@lizhongzhen11 赞同,因为 Object.create(null)返回的是一个没有任何属性的空对象, new Object()返回的是有 __proto__属性的对象,其__proto__指向Object.prototype对象

mqyqingfeng commented 6 years ago

@huangmxsysu @lizhongzhen11 @Latube 非常感谢各位指正!

Latube commented 6 years ago

博主,您好,麻烦您看一下如下代码,我又犯糊涂了。

//为所有函数新增addMethod方法,该方法的目的是为了给构造函数和构造函数的原型对象批量增加方法
Function.prototype.addMethod=function({},{}){
    var funCollection1 = arguments[0];
    var funCollection2 = arguments[1];

    for(name1 in funCollection1){
         this[name1] = funCollection1[name1];
    }
    for(name2 in funCollection2){
         this.prototype[name2] = funCollection2[name2];
    }
}

//定义构造函数
var Methods = function(){};
//对构造函数以及构造函数的原型对象添加方法
Methods.addMethod({
    checkName: function(){
        console.log("checkName func");
    },{
    checkEmail: function(){
        console.log("checkEmail func");
});

//实例化
var m = new Methods()
console.log(m.checkEmail());   //checkEmail func
console.log(m.checkName());  //error

最后一句输出的错误是 m.checkName is not a function ,在对Methods进行功能增强后,为什么实例化后的对象无法复制构造函数里的方法而只能够访问原型对象里的方法呢?(因为是在手机上浏览,代码全是手码上去的)

mqyqingfeng commented 6 years ago

@Latube 上述的代码有的小问题,完整的是不是

   Function.prototype.addMethod = function() {
        var funCollection1 = arguments[0];
        var funCollection2 = arguments[1];

        for (name1 in funCollection1) {
            this[name1] = funCollection1[name1];
        }
        for (name2 in funCollection2) {
            this.prototype[name2] = funCollection2[name2];
        }
    }

    //定义构造函数
    var Methods = function() {};
    //对构造函数以及构造函数的原型对象添加方法
    Methods.addMethod({
        checkName: function() {
            console.log("checkName func");
        }
    }, {
        checkEmail: function() {
            console.log("checkEmail func");
        }
    });

    //实例化
    var m = new Methods()

    console.log(Methods.checkName())

    console.log(m.checkEmail()); //checkEmail func
    console.log(m.checkName()); //error

如果 console.log(Methods.checkName()) 可以打印到 checkName func,这是因为 checkName 方法被挂载到了 Methods 构造函数上,而非 Methods.prototype 上,而实例对象只能访问 Methods.prototype 上的方法,如果 console.log(m.constructor.checkName()) 也是可以打印到的,而为什么只能访问原型对象中的方法,这是因为 JavaScript 原型就是这样设计的……就是只能沿着原型链去查找属性……

Latube commented 6 years ago

@mqyqingfeng 博主,您好,完整的代码确实是您写的这个意思,一下子豁然开朗,感谢😭😭😭

jgchenu commented 6 years ago

从深入第一篇看到这里即将完结,必须给大大点个赞!狂赞一下!写得太好看了,我在坐地铁还有睡觉前,手机微信网页都是置顶大大的博客来看的❤️❤️❤️

xuchaobei commented 6 years ago

第二版代码最后一行, return typeof ret === 'object' ? ret : obj; 应该改为: return ret instanceof Object ? ret : obj; 因为typeof 的值可能等于“function”,如果是函数,依然应该使用这个函数作为最终返回值。

jgchenu commented 6 years ago

@xuchaobei

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;

};

作者写的没有错,如果构造函数里面有返回对象,那么调用构造函数之后返回的对象赋值给 ret,如果没有返回对象,那么默认使用构造的obj,obj的属性通过执行Constructor得来,至于instanceof Object 是为了判断函数返回的值是不是对象,如果返回的值是undefined或者其他基本类型的值,就使用obj作为构造出来的 对象,不知道这么说你是否懂了

lzuliuyun commented 6 years ago

@Latube

顺着你的需求和思路,你是需要修改constructor吧,最后我发现在执行new的时候,其内部是调用最原始的构造函数执行,即便后面修改了构造函数,也没有办法绑定到new出来的实例对象的。

参考:https://stackoverflow.com/questions/9267157/why-is-it-impossible-to-change-constructor-function-from-prototype

自己试着实现了,仅供参考

var util = (function () {      
      function objectFactory() {
        var funCollection1 = arguments[0];
        var funCollection2 = arguments[1];

        return function () {        
          for (name1 in funCollection1) {
              this[name1] = funCollection1[name1];
          }
          for (name2 in funCollection2) {
              this.constructor.prototype[name2] = funCollection2[name2];
          }
        }          
      }

      return {
        objectFactory: objectFactory
      }
 })()

//定义构造函数
var Methods =  util.objectFactory({
  checkName: function() {
      console.log("checkName func");
  }
}, {
  checkEmail: function() {
      console.log("checkEmail func");
  }
});

//实例化
var m = new Methods()

// console.log(Methods.checkName())
console.log(m.checkEmail()); //checkEmail func
console.log(m.checkName()); //checkName func
mqyqingfeng commented 6 years ago

@jgchenu 真的感谢回答哦~ o( ̄▽ ̄)d,不过我发现 @xuchaobei 好像说的没有问题……

当构造函数内部返回一个函数的时候,应该使用这个函数作为返回值,写个 demo 验证一下:

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

    return function cb() {
        return {
            value: 1
        }
    }
}

console.log(new Otaku2('kevin', '18'));  // function cb()

确实是返回了这个函数,所以当返回函数和对象的时候,都应该将其作为最终返回值,使用 typeof 确实没有做到函数的判断……

@xuchaobei 感谢指正~ ( ̄▽ ̄)~*