为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
JavaScript

太行、王屋二山,方七百里,高万仞。本在冀州之南,河阳之北.......

嗯,按照惯例,第一句话就是骗你们点进来的。在读本文之前,希望你对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到底干了什么:

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

以这里的代码为例,实际上第二步和第三步的操作可以总结为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关键字。利用这种模式,我们可以为对象添加额外的能力。本例中,就是给数组添加一个自定义的方法,使其可以拥有我们赋予的新能力。

结语

实际开发中还是得根据实际场景灵活运用,总有适合你的那一款。今天就聊到这,欢迎大家补充和指正。

点击查看更多内容
6人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
6967
获赞与收藏
127

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消