520, 学废 new 对象的过程

9,396 阅读4分钟

前言

众所周知,把大象放进冰箱里需要三步,打开冰箱,放进大象,关上冰箱。那你知道 new 一个对象需要几步?这时候可能就有大聪明小哥哥会说了:“我有五姑娘了,还要啥对象?”咳咳咳...,此对象非彼对象,今天我们讲的是 Javascript 里的对象。

构造函数

在操作 new 对象之前,我们必须要了解一下什么是构造函数。

普通构造函数

构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数就是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数(除了箭头函数,箭头函数不能用作构造器,使用new会抛出错误。),而不使用 new 操作符调用的函数就是普通函数。构造函数首字母通常大写。下面我们来创建一个GirlFirend试试。

function GirlFirend(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    };
    // do something...
}

// 作为构造函数 
let xiaoMei = new GirlFirend('Xiao Mei', 18);
xiaoMei.sayName();	// Xiao Mei


// 作为函数调用
GirlFirend('Xiao Mei', 18)
window.sayName()	// Xiao Mei

关于为什么 Xiao Mei window 跑了,且看“这个"this"到底是什么

我们知道无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。对于上面的例子而言,GirlFirend.prototype.constructor 指向 GirlFirend。我们试着让 GirlFirend 说出自己的爱好。

function GirlFirend(name, age) {
	// do something...
}

GirlFirend.prototype.sayHobby = function () {
    console.log('I like sports');
};

let xiaoMei = new GirlFirend('Xiao Mei', 18);
xiaoMei.sayHobby();	// I like sports

温馨提醒:你的对象可能不是表面看到的那样,我们来看下面的例子

function GirlFirend(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    };

    // 如果这里 return 'Tomboy',最后的结果还是 'Xiao Mei'
    return {
        name: 'Tomboy',
        age: 30,
        sayName: function() {
            console.log(this.name)
        }
    }
}

let xiaoMei = new GirlFirend('Xiao Mei', 18);
xiaoMei.sayName();	// Tomboy,这里的对象就成了构造函数返回的对象

类构造函数

在类定义块内部,constructor 关键字用于创建类的构造函数。方法名 constructor 会告诉解释器 在使用 new 操作符创建类的新实例时,应该调用这个函数。

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。

class GirlFirend {
    constructor(name) {
        // 添加到 this 的所有内容都会存在于不同的实例上
        this.name = name
    }
}
let xiaoMei = new GirlFirend('Xiao Mei');
console.log(xiaoMei.name);          // Xiao Mei

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则会通过原型对象上找到属性后,再返回对应的值

class GirlFirend {
    constructor(name) {
        // 添加到 this 的所有内容都会存在于不同的实例上
        this.name = name
    }
    // 定义在类的原型对象上
    sayName() {
        console.log(this.name);
    }
    // 定义在类本身上
    static sayHobby() {
        console.log('I like sports');
    }
}
let xiaoMei = new GirlFirend('Xiao Mei');
console.log(xiaoMei.name);          // Xiao Mei
xiaoMei.sayName();                  // Xiao Mei
GirlFirend.sayHobby();              // I like sports

注意:类中定义的静态方法只是在类本身上,不存在实例或者原型上。

new 的作用

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

从前面的几个示例来看,不难看出使用 new 调用构造函数会执行如下步骤。

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

[[prototype]]属性是隐藏的,不过目前大部分新浏览器实现方式是使用__proto__来表示。构造函数的prototype 属性我们是可以显式访问的。

实现 new

最后我们按照前面的思路自己动手实现一个 new 吧。注意: ES6 class 必须是通过 new 来实例化

function _new(fn, ...arg) {
    const obj = Object.create(fn.prototype);
    const newObj = fn.apply(obj, arg);
    return newObj instanceof Object ? newObj : obj;
}

// 使用的例子:
function GirlFirend(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    };
}
const xiaoMei = _new(GirlFirend, 'Xiao Mei', 18)
console.log(xiaoMei)  // GirlFirend {name: "Xiao Mei", age: 18, sayName: ƒ}

Object.create(proto, [propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是 undefined)。

完。