用愚公移山说明Javascript创建对象的各种姿势

都退后,我要继续讲故事了。

北山愚公者,年且九十,面山而居。

var person = {
    name : '愚公',
    age: 90,
    address: '北山脚下',
    whereToLive: function () {
        alert(this.address)
    }
};

…北山愚公曰:“虽我之死,有子存焉;子又生孙,孙又生子;子又有子,子又有孙;子子孙孙无穷匮也”。

看到这儿,问题来了,愚公的子子孙孙那么多,显然使用对象字面量去创建是不合理的。我们介绍第一种创建方式。

工厂模式

function createPerson (name, age, address){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.address = address;
    o.whereToLive = function () {
        alert(this.address)
    };
    return o;
}

var son = createPerson('愚小公', 30, '北山');
var grandSon = createPerson('愚小小公', 5, '北山');

工厂模式比较明显的一个缺点就是由于生成并返回了一个中间对象,所以不能判断对象的类型。

构造函数模式

function Person(name, age, address) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.whereToLive = function(){
            alert(this.address);
        }; 
}
var son = new Person('愚小公', 30, '北山');
var grandSon = new Person('愚小小公', 5, '北山');

构造函数与普通函数没有异处,没有语法上的任何差别,只是在调用的时候使用了new关键字。所以我们有必要说一下new到底干了什么:

  • 创建一个新的中间对象
  • 将构造函数的作用于赋给这个中间对象
  • 执行构造函数中的代码
  • 返回中间对象

以这里的代码为例,实际上第二步和第三步的操作可以总结为Person.apply(newObject,arguments),这里顺便说一句bind与call/apply的一个区别,bind返回的是一个函数,call/apply是顺带把这个函数给执行了,返回的是执行后的结果。

那么,构造函数模式有什么问题呢,其实也是显而易见的,如果愚公有一千个子子孙孙,那么每个子孙都会自带一个whereToLive的方法,显然这种做法不文艺范儿

原型模式

function Person () {
    
}

Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = '北山';
Person.prototype.whereToLive = function () {
    alert(this.address); 
};

var son = new Person();
var grandSon = new Person();
son.name = '愚小公';
son.address = '山的那边';


son.whereToLive();   //  '山的那边'
grandSon.whereToLive();   //  '北山'

我们在son对象上试图修改address属性,并且似乎看起来也修改成功了,但是没有影响到grandSon的属性。所以其实这两个address其实并不一样。为什么呢?我们在做如下操作:

delete son.address;
son.whereToLive();   //  '北山'

我们删掉了son的address属性,这时候son的address又成了原型中定义的值。所以我们在修改address属性的时候并没有动到原型中的值,而是在这个对象上新建了一个属性。并且在试图获取这个属性的时候会优先返回对象上的属性值。我们管这个现象叫属性屏蔽。

另外多提一点,就是在读取对象属性的时候,首先会查看该对象本身有没有,没有的话会顺着原型链一直向上查找,如果达到原型链顶层都没有找到,则返回undefined。这里再穿插一个知识点。很多刚入门的开发者会犯这样的错误:

var a = {};
console.log(a.b.c)

在没有校验b属性是否存在便去试图获取c属性。如果到了原型链的顶端都没有找到b,a.b的值则为undefined,所以获取undefined的c属性一定会报错。正确的做法是在不确定是否存在对应属性的时候,应当先做判断。

但是在写入基本类型属性的时候有所不同,在当前对象没有找到要写入的属性时,不会向上查找,而是在当前对象里新建一个属性,这么做的原因是防止污染其他对象的属性值。细心的你可能发现了我在开头的时候强调了基本类型属性。如果是引用类型会怎么样呢?

function Person () {
    
}

Person.prototype.name = '愚公';
Person.prototype.age = 90;
Person.prototype.address = ['北山'];
Person.prototype.whereToLive = function () {
    alert(this.address); 
};

var son = new Person();
var grandSon = new Person();
son.address.push('山的那边');

grandSon.whereToLive();   //  '北山','山的那边'

这里又有一个小知识点,引用类型是存在堆内存中的,不同地方的应用其实指向的是同一块堆内存。所以如果试图修改原型对象中的应用类型,会造成全局污染,这也就是原型模式的一个致命缺点。

组合使用构造函数模式和原型模式

坐稳,我又要穿插新的知识点了。我们可以采用简写的方式避免原型模式赋予原型对象方法时啰嗦的问题。

function Person(name, age, address) {
        this.name = name;
        this.age = age;
        this.address = address;
}
Person.prototype = {
    constructor : Person,  // 手动修改构造函数指向
    whereToLive : function () {
        alert(this.address); 
    },
    howOld : function () {
        alert(this.age); 
    }
}

组合使用构造函数模式和原型模式的写法是不是同时规避掉了构造函数模式和原型模式的问题呢?既可以共享公用的函数,又可以让每个对象独享自己的属性。

需要注意的是,我们在重写Person.prototype的时候,实际上使得constructor指向了Object,所以我这里进行了手动修正。

寄生构造函数模式

function PersonList (name, age, address){
    var o = new Array();
    o.push.apply(o, arguments);
    o.consoleString = function () {
       return this.join(",");
    };
    return o;
}

var list = new PersonList('愚小公', '愚小小公');
alert(list.consoleString());

是不是很眼熟,跟工厂模式一模一样,只不过是在调用的时候使用了new关键字。利用这种模式,我们可以为对象添加额外的能力。本例中,就是给数组添加一个自定义的方法,使其可以拥有我们赋予的新能力。

实际开发中还是得根据实际场景灵活运用,总有适合你的那一款。

最后

为了帮助大家让学习变得轻松、高效,给大家免费分享一大批资料,帮助大家在成为全栈工程师,乃至架构师的路上披荆斩棘。在这里给大家推荐一个前端全栈学习交流圈:866109386.欢迎大家进群交流讨论,学习交流,共同进步。

当真正开始学习的时候难免不知道从哪入手,导致效率低下影响继续学习的信心。

但最重要的是不知道哪些技术需要重点掌握,学习时频繁踩坑,最终浪费大量时间,所以有有效资源还是很有必要的。

最后祝福所有遇到瓶疾且不知道怎么办的前端程序员们,祝福大家在往后的工作与面试中一切顺利。

猜你喜欢

转载自blog.csdn.net/q3254421/article/details/89477282