mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。
30.75k stars 4.71k 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,对作者也是一种鼓励。

mqyqingfeng commented 6 years ago

@lzuliuyun 赞 o( ̄▽ ̄)d

hsroad commented 6 years ago

你好,new 模拟实现的话 是不是还应该有一个是判断返回的是不是对象

GayeChen commented 6 years ago

var obj = new Object(), 这里岂不是用了new,改成字面量创建,也行吧 var obj = {}

Rabbitzzc commented 6 years ago

@mqyqingfeng 对于let obj = new Object()已经使用了new去创造一个实例,那可否直接使用let obj = {}呢

hankanon commented 6 years ago

虽然是写new关键字的实现原理 但是懂得了Object.create() 和 new Object() 的区别,很开心

Rabbitzzc commented 6 years ago

是的,通过

let a = new Object()
let b = Object.create(null)
let c = {}
打印的a,b,c可以看到,b不存在__proto__这个属性,而其他两个则存在。
haoxl3 commented 6 years ago

所以最后一句应该改为:return ret instanceof Object ? ret : obj; 是吗?小白我都看晕了

TStoneLee commented 5 years ago

@haoxl3 应该使用instanceof判断 不使用的话,如果是返回值是function类型的话,自定义的new会返回一个被赋值的构造函数(?是这样叫吗) Otaku { name: 'snow', age: 23, habit: 'Games' }
function objectFactory() { var obj = new Object(); var Constractor = [].shift.call(arguments); obj.proto = Constractor.prototype; // obj = Object.create(Constractor.prototype) var ret = Constractor.apply(obj, arguments); return typeof ret === 'object' ? ret || obj : obj; // return ret instanceof Object ? ret || obj : obj; } function Otaku(name, age) { this.name = name; this.age = age; this.habit = 'Games' return function() { value = 1; } } Otaku.prototype.strength = 60; Otaku.prototype.sayYourname = function() { console.log('I am ' + this.name); } var person = objectFactory(Otaku, 'snow', 23) person.sayYourname() console.log(person)

如果使用instanceof判断的话,会出现和原生的new 一样的结果。 即, 会报错,TypeError: person.sayYourname is not a function。(仅在上面的实例中)

SageWu commented 5 years ago
function objectFactory() {
    var constructor = [].shift.call(arguments);
    var obj = Object.create(constructor.prototype);
    var result = constructor.apply(obj, arguments);
    return result instanceof Object? result: obj;
};
MiaLeung01 commented 5 years ago

赞,以前只知道 call apply bind new 这些方法,看完深入了解了他们是怎么实现的了!

imtag commented 5 years ago

var obj = Object.create(null); obj.proto = Constructor.prototype;

看了上面议论通过 Object.create(null) 创建一个空对象,但我发现实际上这种方式实例没法访问到 Constructor.prototype 上面的属性。

roninliu commented 5 years ago

var obj = Object.create(null); obj.proto = Constructor.prototype;

看了上面议论通过 Object.create(null) 创建一个空对象,但我发现实际上这种方式实例没法访问到 Constructor.prototype 上面的属性。

Object.create(null)继承的null的原型,所以没有prototype

imtag commented 5 years ago

是没有prototype,难道通过给obj添加--proto--属性也没用吗。

ilvliu commented 5 years ago

很赞了

SanQiG commented 5 years ago

大大你好,我想share一下我在看您文章之前所知道的new的底层实现

function New(fn) {
    var res = {};
    res.prototype = fn.prototype;

    var ret = fn.apply(res, Array.prototype.slice.call(arguments, 1));

    if ((typeof ret == 'function' || typeof ret == 'object') && ret !== null) {
        return ret;
    }

    return res;
}
quanru commented 5 years ago

@jawil

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

这行是不是多余了

zhw2590582 commented 5 years ago

ES6 简化版

function objectFactory(Constructor, ...rest) {
  const instance = Object.create(Constructor.prototype);
  const result = Constructor.apply(instance, rest);
  return (typeof result === 'object' && result) || instance;
}
inkn commented 5 years ago

没有明白为什么 Constructor 会声明成全局变量

ositowang commented 5 years ago

有点不明白为什么这里后来直接在构造函数原型链上加的函数,就取不到了,显示fuction is not defined

const objectContructorUltimate = (...args) => {
  let constructor = [].shift.call(args);
  let obj = Object.create(constructor.prototype);
  let result = constructor.apply(obj, args);
  // ensure we return an object or null
  return typeof result === 'object' ? result : obj;
};

//your lovely test cases
function cat(name, sex) {
  this.age = 16;
  this.weight = 20;
  return {
    name: name,
    sex: sex,
  };
}

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

const kathy = objectContructorUltimate(cat, 'kathy', 'female');
console.log(kathy.name);
console.log(kathy.sex);
kathy.meow() // meow is not defined
zhw2590582 commented 5 years ago

@ositowang 因为你的cat构造函数返回了一个对象,实例化之后也只会返回这个对象。

cliYao commented 5 years ago

楼上正解,如果没有返回对象的话,就可以访问到原型对象上新增的方法

FishPlusOrange commented 5 years ago

没有明白为什么 Constructor 会声明成全局变量

正文例子里面 Constructor 并不是声明成全局变量,只是省略了 var

它的上一句 var obj = new Object(), 是以 , 结尾,而不是 ;

xukuanzhuo commented 5 years ago

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

感觉根本原因有误。

原因是 Object.create(null) 创建的是一个没有原型链链接的空对象, new Object() 类似于 Object.create(Object.prototype) 创建的是有原型链链接的空对象,

var a = Object.create(null)
var b = new Object()

Object.getPrototypeOf(b) === Object.prototype // true

a 是没有原型链链接的空对象,自然访问不到__proto__ 属性,没有在原型链上,所以也可以用来做字典,不会有原型链上属性影响。

reference: You Don't Know JS Object.create

GisonL commented 5 years ago
return typeof ret === 'object' ? ret : obj;

这里不严谨:

  1. 如果构造函数return的是函数的话,接收到的也会是那个函数。 test2

  2. 如果return null,也应该返回obj

    所以我认为应该写成:


return (typeof ret === 'object' || typeof ret === 'function')? ret||obj : obj;
yaozhongnan commented 5 years ago

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

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

两年了...

Arrogant128 commented 5 years ago

image 这种方式可以不

btea commented 5 years ago

@Arrogant128 你的fn执行结果如果返回的是一个非null的对象,mvmv的值应该指向该返回对象。

Arrogant128 commented 5 years ago

@btea 我myNew方法写错了,使用Object.create(null)创造出来的对象没有proto属性,应该直接空对象就好了

Lin-Saint commented 5 years ago

两年了,我还在等你的react哈哈哈哈哈

wangyesheng commented 5 years ago

两年了,我还在等你的react哈哈哈哈哈

vnues commented 5 years ago

哈哈哈哈哈 想到一个问题 我们是去实现new 还用 var obj = new Object() 这不是有毒吗

实际let obj={} <===>var obj = new Object() 这样理解起来不是更好吗哈哈哈哈哈哈

naecoo commented 5 years ago

function objectFactory (o, ...args) { const c = Object.create(o.prototype) c.proto.constructor = o o.apply(c, args) return c }

raclen commented 5 years ago

请问这么写有什么问题吗,我看起来也能用

    function Student(name, age, sex) {
      People.call(this, name, age);
      this.sex = sex;
    }
    Student.prototype.speak = function() {
      return "i im speak";
    };
   function customizeNew(Sup) {
      let Sub = {}; //新建一个对象
      Sub.__proto__ = Sup.prototype; //对象的原型指向其构造函数
      Sub.constructor = Sup;
      return function() {
        Sup.apply(Sub, arguments);
        return Sub;
      };
    }
    let s2 = customizeNew(Student)("悟空", 24);
    let s3 = customizeNew(Student)("悟空1", 25);
luckymore commented 5 years ago

@jawil @mqyqingfeng @Imlisten

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,也来实践模拟一把😄

这一句没啥用吧

var obj = new Object(),//从Object.prototype上克隆一个对象
usherwong commented 5 years ago

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

 let obj = Object.create(Constructor.prototype)

这样和楼主的

obj.__proto__ = Constructor.prototype;

效果是一样的

MrRENGE commented 5 years ago

博主使用 var obj = new Object() ,是为了返回的对象的__proto__属性能指向Object目的也许是为了保持一致的表现。评论区有老哥说的使用var obj = Object.create(null),表现是不一样的,obj上不存在任何属性此obj.__proto__则为undefined。其实说var obj = {};var obj = new Object(); var obj = Object.create(); 效果都是一样的,既然为了模拟new,封装的时候不使用new跟合理些。强迫症患者0_0

YuArtian commented 5 years ago

最后的判断可以改成 ret instanceof Object ? ret : obj

xsfxtsxxr commented 4 years ago

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

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

大佬,现在2019年了,React你还没开始写呢,没事,我还能再等两年

tinytot1 commented 4 years ago

根据mdn new 运算符定义

/**
 * 创建一个空的简单JavaScript对象(即{});
 * 链接该对象(即设置该对象的构造函数)到另一个对象 ;
 * 将步骤1新创建的对象作为this的上下文 ;
 * 如果该函数没有返回对象,则返回this。
 */

const _new = (_Contructor, ...args) => {
  // 1.创建一个空的简单JavaScript对象(即{});
  const obj = {}
  // 2.链接该对象(即设置该对象的构造函数)到另一个对象 ;
  Object.setPrototypeOf(obj, _Contructor.prototype)
  // 3.将步骤1新创建的对象作为this的上下文 ;
  const ret = _Contructor.apply(obj, args)
  //4.如果该函数没有返回对象,则返回this。
  return ret instanceof Object ? ret : this
}
MierCurry commented 4 years ago

有个问题,就是这个是模拟new的实现,然后new Object()这个不是已经用了new了吗

sfsoul commented 4 years ago

有个问题,就是这个是模拟new的实现,然后new Object()这个不是已经用了new了吗

你看下其他人的回复,修改下定义就好了。let obj = {}

ghost commented 4 years ago

发一下我写的版本,

Function.prototype._newOne = function () {
    let cotr = this;
    let newObj = Object.create({});
    Object.setPrototypeOf(newObj, cotr.prototype);
    let args = [].slice.call(arguments);
    let res = cotr.apply(newObj, args);
    function isObjectOrFunction (res) {
        return (typeof res === 'object' || typeof res === 'function');
    }
    return isObjectOrFunction(res) ? res || newObj : newObj;
}

function P (name) {
    this.name = name;
}

P.prototype.age = 22;

var p = P._newOne('linyx');

p.name // 'linyx'
p.age // 22
p.__proto__ === P.prototype // true

大家看看这么写有什么问题吗

ATQQ commented 4 years ago

依葫芦画瓢,我也丢一个小改版

function myNew() {
    // 创建一个空对象
    let obj = {}
    // 获取构造函数
    let Con = [].shift.call(arguments)
    // 设置空对象的原型
    obj.__proto__ = Con.prototype
    // 绑定this
    let res = Con.apply(obj, arguments)
    // 返回新对象
    return res instanceof Object ? res : obj
}
lisiur commented 4 years ago

根据mdn new 运算符定义

/**
 * 创建一个空的简单JavaScript对象(即{});
 * 链接该对象(即设置该对象的构造函数)到另一个对象 ;
 * 将步骤1新创建的对象作为this的上下文 ;
 * 如果该函数没有返回对象,则返回this。
 */

const _new = (_Contructor, ...args) => {
  // 1.创建一个空的简单JavaScript对象(即{});
  const obj = {}
  // 2.链接该对象(即设置该对象的构造函数)到另一个对象 ;
  Object.setPrototypeOf(obj, _Contructor.prototype)
  // 3.将步骤1新创建的对象作为this的上下文 ;
  const ret = _Contructor.apply(obj, args)
  //4.如果该函数没有返回对象,则返回this。
  return ret instanceof Object ? ret : this
}

@tinytot1 最后应该返回 obj 吧

hardmanhong commented 4 years ago

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

感觉这样好一点

var obj = Object.create(Constructor.prototype)
liubon commented 4 years ago

Constructor 这不是声明了一个全局变量么?在外部可以访问到啊,是不是要加个声明

angelayun commented 4 years ago

根据mdn new 运算符定义

/**
 * 创建一个空的简单JavaScript对象(即{});
 * 链接该对象(即设置该对象的构造函数)到另一个对象 ;
 * 将步骤1新创建的对象作为this的上下文 ;
 * 如果该函数没有返回对象,则返回this。
 */

const _new = (_Contructor, ...args) => {
  // 1.创建一个空的简单JavaScript对象(即{});
  const obj = {}
  // 2.链接该对象(即设置该对象的构造函数)到另一个对象 ;
  Object.setPrototypeOf(obj, _Contructor.prototype)
  // 3.将步骤1新创建的对象作为this的上下文 ;
  const ret = _Contructor.apply(obj, args)
  //4.如果该函数没有返回对象,则返回this。
  return ret instanceof Object ? ret : this
}

ret instanceof Object 这里不行,如果


const _new = (_Contructor, ...args) => {
  // 1.创建一个空的简单JavaScript对象(即{});
  const obj = {}
  // 2.链接该对象(即设置该对象的构造函数)到另一个对象 ;
  Object.setPrototypeOf(obj, _Contructor.prototype)
  // 3.将步骤1新创建的对象作为this的上下文 ;
  const ret = _Contructor.apply(obj, args)
  //4.如果该函数没有返回对象,则返回this。
  return ret instanceof Object ? ret : this
}

代码对于 proto的设置是不正确的,你用下面的测试代码试下就知道了 // 下面是测试代码 function Person (name, age) { this.name = name; this.age = age;

return {
    name: name,
    habit: 'read book'
}

}

Person.prototype.strength = 60;

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

var person = _new(Person, 'angela', '20')

console.log(person.name) console.log(person.habit) console.log(person.strength)

person.sayName();

wungjyan commented 4 years ago

@mqyqingfeng 返回值那里有问题,我改了下:

function myNew (){
    const obj = {}
    Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype
    const ret = Constructor.apply(obj, arguments)
    // 实测,当构造函数返回对象或者函数时,直接返回;如果为null或者基本类型,则返回实例对象
    if(
        (typeof ret === 'object' || typeof ret === 'function')
        && ret !== null
    ){
        return ret
    }
    return obj
}
FridayDai commented 4 years ago

写一个便于理解的版本

function myNew(fn, ...args) {
  const obj = Object.create(fn.prototype);
  const result = fn.call(obj, ...args);
  return typeof result === 'object' ? result || obj : obj;
}
Vstar18 commented 4 years ago

@mqyqingfeng 关于Object.create(null)的猜想我同意,使用Object.keys验证一下。person1是Object.create({})生成的,person2是Object.create(null)生成的。结果如下: image

猜想: Object.create(null)是创建一个原型链指向null的对象,当原型链为null时,不可再次更改原型链。因此再次obj.proto 赋值时,实际是添加了一个属性,而不是链接原型了。

原因可能是原型链的顶端就是null(Object.prototype.proto === null//true),所以原型链是null的不可更改


@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