程序员React面试经

谈谈对象之创建对象

2018-06-18  本文已影响91人  D6K

前端三年,一直以来忽略了对象的重要性,导致根基不稳,容易弄混很多关系。最近刚辞职,准备面试,终于可以抽出点时间是时候谈谈对象了。
在经典的面向对象编程语言(如Java)中,对象是由类创建的实例。然而在JS中却没有类的概念,虽然ES6中引入了一套关键字来实现class,使得编写的代码很接近基于类的代码,但是这只是一个语法糖而已,JS仍然是基于原型的。
在JS中对象就是一组无序的属性集合,其属性值可以为基本值、函数或者对象。可以简单的理解为是一组键值对的集合,其值可以是数据或者方法。JS中的对象相当于经典面向对象语言中的类,是创建对象实例的模板。
JS中创建对象的方式有很多,也很复杂,本文的主要内容就是总结JS中创建对象的各种姿势,为理解JS的继承实现打下基础。(其中大多数是学习《JavaScript高级程序设计》的)

下面直接摆出各种创建对象的姿势,欢迎补充!

1. 通过Object构造函数创建

var person = new Object();
person.name = "Dreamer King";
person.age = 20;
person.showInfo = function(){
    console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
};

console.log(person.name);
person.showInfo();

2. 通过字面量的方式创建

var person = {
    name: "Dreamer King",
    age: 20,
    showInfo: function(){
        console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
    }
};

console.log(person.name);
person.showInfo();

通过字面量的方式创建对象看上去比通过Object构造函数创建的方式更紧凑简洁,因而颇受欢迎。
以上两种方式主要用来创建单个对象,若要创建多个对象将会产生大量重复的代码。这是不可接受的,我们可以采用工厂模式来抽象对象的创建具体过程,通过传入必要的参数进去,生成所要的对象。

3. 使用Object.create()方法创建对象

Object.create(proto,[propertiesObject])方法可以使用现有对象来提供创建新对象的__proto__,创建一个新的对象。其中第一个参数为新创建对象的原型对象,第二参数可选,是要添加到创建对象的可枚举属性对象的属性描述符以及相应的属性名称。

var person = {
    name: "King",
    age: 20,
    showInfo() {
        console.log(this.name, this.age);
    }
};

var me = Object.create(person,{
    job: {
        writeable: true,
        configurable: true,
        value: "Front End Enginger"
    }
});

me.showInfo();
me.name = "Dreamer";
me.showInfo();
console.log(me.job);
person.showInfo();
使用Object.create()方法创建对象

4. 使用class创建对象

ES6中新增了class相关的关键字,使得对象的创建更加接近经典的面向对象编程模型,这种创建的对象的方式代码简洁明了,是今后JS编程潮流。

class Person {
    constructor() {
        this.name = "Dreamer";
        this.age = 21;
    }

    static hello() {
        console.log("hello");
    }

    showInfo() {
        console.log(this.name, this.age);
    }
}

var  p = new Person();
p.showInfo();
Person.hello();
使用class创建对象

5. 采用工厂模式创建对象

function createPerson(name, age) {
    return {
        name,
        age,
        showInfo() {
            console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
        }
    };
}

var me = createPerson("Dreamer", 20);
console.log(me.name, me.age);
me.showInfo();

var you = createPerson("King", 16);
you.showInfo();

使用工厂模式创建对象虽然解决了大量相似对象的创建问题,但是却没有解决对象识别的问题。因此,出现了构造函数模式。

6. 使用构造函数创建对象

使用构造函数可以创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,另外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showInfo = function() {
        console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
    };
}

var me = new Person("Dreamer", 20);
console.log(me.name, me.age);
me.showInfo();

var you = new Person("King", 16);
you.showInfo();

console.log(me.constructor === you.constructor);
console.log(me.constructor === Person);
console.log(me instanceof Person);
使用构造函数创建对象

构造函数模式与工厂模式很类似,但也有不同之处。

使用构造函数创建对象实际上会经历一下四个步骤:

  1. 创建一个新对象;
  2. 将构造函数作用域赋给新对象(因此this指向了这个对象);
  3. 执行构造函数代码(为对象添加属性和方法)
  4. 返回新对象。

对象的constructor属性最初是用来标识对象类型的,这就意味着创建自定义的构造函数可以作为对象实例的一种特定类型的标识,这也就是构造函数胜出工厂模式的地方。
另外,构造函数与普通函数的唯一区别就是调用方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符调用,那它就可以作为构造函数,而如果不通过new操作符调用,那它跟其他普通函数也没什么两样。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showInfo = function() {
        console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
    };
}

var me = new Person("Dreamer", 20);
me.showInfo();

Person("king", 16);
global.showInfo(); //全局对象调用

var o = Object();
Person.call(o, "DK", 32); //另一个对象调用 
o.showInfo();

构造函数与普通函数

构造函数虽然好用,但也不是没有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍,然而这是不必要的,况且有this对象在,根本不用在执行代码之前就把函数绑定到特定的对象上。于是可把构造函数内定义的函数,转到构造函数外来解决这个问题。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showInfo = function() {
        console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
    };
}

var me = new Person("Dreamer", 20);
var you = new Person("King", 16);
console.log(me.showInfo == you.showInfo); // false 说明函数没有共享
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showInfo = showInfo;
}

function showInfo() {
    console.log(`I'm ${this.name}, I'm ${this.age} old year. `);
};


var me = new Person("Dreamer", 20);
var you = new Person("King", 16);
console.log(me.showInfo == you.showInfo);
me.showInfo();
you.showInfo();
将构造函数中的方法转移到构造方法外实现共享

将方法转移到构造函数外确实解决了方法共享的问题,但是它变成了全局函数了,在全局作用域中定义了实际上只被某个对象调用的函数,这使得全局作用域有点名不副实了。更无法接受的是:如对象需要定义很多方法,那么要在全局作用域内定义很多全局函数,这使得自定义的类型毫无封装性可言了。幸好,这个问题可以使用原型模式来解决。

6. 使用原型模式创建对象

1. 在原型对象上逐个新增属性的方式

每个函数都一个prototype属性,这个属性指向包含可以由特定类型的所有实例共享的属性和方法组成的对象,也就是通过调用构造函数而创建的那个对象实例的原型对象。使用用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。因此可以将所有共享的属性和方法直接添加到原型对象中,而将不共享的部分放到对象实例中。

function Person() {
}

Person.prototype.name = "Dreamer";
Person.prototype.age = 20;
Person.prototype.showInfo = function() {
    console.log(this.name, this.age);
}

var person1 = new Person();
person1.showInfo();
var person2 = new Person();
person2.showInfo();

console.log(person1.showInfo === person2.showInfo);
console.log(person1 === person2);
console.log(Person.prototype.constructor === Person);
console.log(person2.__proto__ === Person.prototype);
通过原型对象共享数据和方法

每当访问对象的某一属性或方法时,都会执行一次搜索,然后返回给定名称的属性或方法。搜索首先从对象实例本身开始,若找到就返回该值,所没有找到则继续搜索指针指向的原型对象继续搜索,如此逐级搜索,直至找到或者搜索指针指向null。这便是多个对象共享原型对象所保存的属性和方法的基本原理。
虽然可以通过对象实例访问原型中的值,但是却不能通过对象实例访问重写了的原型中的值。这因为在实例中添加与实例原型相同的属性名会在实例中创建该属性,从而屏蔽了原型中的那个同名属性。

function Person() {
}

Person.prototype.name = "Dreamer";
Person.prototype.age = 20;
Person.prototype.showInfo = function() {
    console.log(this.name, this.age);
}

var person1 = new Person();
person1.name = "King";
person1.showInfo();
var person2 = new Person();
person2.showInfo();

实例上的属性屏蔽原型上的同名属性

2. 给原型对象字面赋值的方式

function Person() { }

Person.prototype = {
    name: "Dreamer",
    age: 20,
    showInfo() {
        console.log(this.name, this.age);
    }
};

var dm = new Person();
dm.showInfo();
console.log(dm.constructor === Person);
console.log(dm instanceof Person);
重写prototype对象

以上重写了prototype对象,导致其constructor属性发生了改变。若constructor比较重要,则可将其置回适当的值。

function Person() { }

Person.prototype = {
    constructor: Person,
    name: "Dreamer",
    age: 20,
    showInfo() {
        console.log(this.name, this.age);
    }
};

var dm = new Person();
dm.showInfo();
console.log(dm.constructor === Person);
console.log(dm instanceof Person);
console.log(Object.getOwnPropertyDescriptor(dm.__proto__,'constructor'));
重置contructor属性改变了constructor属性的特性

但是上面的方式重置constructor属性会导致它的[[Enumerable]]特性被设置为true,然而默认情况下constructor属性是不可枚举的。因此可以考虑用Object.defineProperty()对constructor重置解决这个问题。

function Person() { }

Person.prototype = {
    name: "Dreamer",
    age: 20,
    showInfo() {
        console.log(this.name, this.age);
    }
};

Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
});

var dm = new Person();
dm.showInfo();
console.log(dm.constructor === Person);
console.log(dm instanceof Person);
console.log(Object.getOwnPropertyDescriptor(dm.__proto__,'constructor'));

不改变constructor特性的重置方式

原型模式也不是没有缺点,首先,它省略了为构造函数传递初始化参数的环节,结果导致所有实例默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一定的不便,但还不原型模式的最大问题。原型模式的最大问题是由其共享的本性所致的。原型中所有属性是被许多属性所共享的,这种共享对于函数非常合适。对于包含基本值得属性可通过在实例添加同名属性将原型中的属性给屏蔽掉,但是对于引用类型值的属性却有可能存在不该共享的属性却共享了的问题。

function Person() { }

Person.prototype = {
    constructor: Person,
    name: "Dreamer",
    age: 20,
    friends: ["XuYing","Trump"],
    addFriends(friend) {
        this.friends.push(friend);
    }
};

var person1 = new Person();
var person2 = new Person();
person2.addFriends("AiLen");
console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends);
不该共享的属性却共享咯

为了让实例具有共享属性同时也有自己属性,所一般很少单独使用原型模式,一般使用构造函数与原型的组合的模式。

7. 构造函数与原型组合模式

这种组合方式是创建自定义类型最常用的模式。构造函数用于定义实例属性,而原型用于定义方法和共享的属性。结果,每个实例都会拥有自己的一份实例属性副本,同时又可以共享对方法的调用和共享属性,最大限度地节省内存。这是目前使用最广泛、认同度最高的一种创建自定义类型的方法,可以说是用来定义引用类型的一种默认模式。

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.friends = ["XiaoZhuzi", "King"]
}

Person.prototype = {
    constructor: Person,
    showFriends: function() {
        console.log(this.friends.toString());
    }
}

var person1 = new Person("Dreamer", 21);
var person2 = new Person("Zhaopp", 16);

person2.friends.push("Dreamer");

person1.showFriends();
person2.showFriends();

console.log(person1.showFriends === person2.showFriends);
console.log(person1.friends == person2.friends);
构造函数与原型组合模式

8. 动态原型模式

组合构造函数和原型的模式,其代码是分开的,很可能会感到困惑。动态原型模式就是致力于解决这个问题的一个方案,它把所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持同时使用构造函数和原型的优点。也即是通过检查某个方法是否有效来决定是否需要初始化原型。

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

    if(typeof this.showInfo != 'functon') {
        Person.prototype.showInfo = function() {
            console.log(this.name, this.age);
        }
    }
}

var person = new Person("Dreamer", 12);
person.showInfo();

9. 寄生构造函数模式

寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是分装创建对象的代码,然后返回新创建的对象。注意:寄生构造函数模式创建的对象与构造函数和构造函数的原型属性之间没有关系,也就是说寄生构造函数返回的对象与构造函数在外部创建对象没有什么不同,为此,不能依赖于instanceof操作符来确定对象类型,所一般不采用这种模式。

function Person(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age; 
    o.showInfo = function() {
        console.log(this.name, this.age);
    }
    return o;
}

var me = new Person("King", 21);
me.showInfo();
console.log(me instanceof Person);
寄生构造函数创建的对象其类型不能依赖于instanceof确定

10. 稳妥构造函数模式

稳妥对象是指没有公共属性,而且其方法也不引用this的对象。最适合在一些安全的环境中(这些环境会禁止使用new和this)或者防止数据被其他应用改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点不同:一是创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。

function Person(name, age) {
    var o = new Object();
    o.showInfo = function() {
        console.log(name, age)
    };
    return o;
}

var person = Person("King", 12);
person.showInfo();

以上展示并总结了十多中创建对象的方式,不同的方式有不同的使用场景。有没有感到JS原来还可以这么灵活,多姿多彩啊!

上一篇下一篇

猜你喜欢

热点阅读