JS 之 创建对象

虽然我们可以通过Object构造函数或者字面量的方式创建单个对象,但它有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

接下来我们来讲解一下创建对象的四种方式。

1、工厂模式

工厂模式是一种非常重要的设计模式,有很多实际应用。在ECMAScript中无法创建类,但可以通过工厂模式来抽象具体对象的创建过程,用工厂函数封装以特定接口创建对象的细节 。

function createPerson(name,age,sex){
    var o=new Object();
    o.name=name;
    o.age=age;
    o.sex=sex;
    o.sayName=function(){
        alert(o.name);
    }
    return o;
}

var p1=createPerson("rabbit",18,"female");

工厂模式做到了能够批量生成许多相识的对象,但是仍然没有“类”的概念,无法鉴别对象的类型。

2、构造函数模式

ECMAScript的构造函数可用来创建特定类型的对象。像Object、String、Array等都是。

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

var p=new Person("rabbit",18,"female");
console.log(p instanceof Object);            //true
console.log(p instanceof Person);            //true

console.log(p.constructor);                  //打印出其构造函数的定义

通过构造函数创建的对象有了类的概念,可以使用 instanceof检测

可以看出与工厂模式的不同之处:

1、没有显式的创建对象(后台会自动创建)

2、直接将参数赋给了this

3、没有return (后台自动返回)

4、通过 new 来创建对象

用 new 操作符调用构造函数会经历以下四个步骤:

1、创建一个新对象

2、将构造函数的作用域(执行环境)赋给新对象(this就指向了这个新对象)

3、执行构造函数中的代码(给新对象添加属性函数啥的)

4、返回这个新对象

通过构造函数创建的对象实例有一个constructor属性(默认不可枚举),该属性指向构造函数。

构造函数和普通函数的不同之处就是调用方式不同,但构造函数也是函数,也可像普通函数那样使用。任何函数只要通过new来调用,都可成为构造函数。看下面的例子:

//构造函数同上

var p1=new Person("Rabbit",18,"female");

Person("Lisi",30,"male");          //当做普通函数来调用,那么构造函数中的this指的就是window对象
window.sayName();                  //返回 "Rabbit"

var o=new Object();
Person.call(o,"ZhangSan",20,"female");
o.sayName();                       //返回 "ZhangSan"

也可以在使用call某个特殊的作用域下调用Person函数,于是this就指向了对象o。

构造函数的问题:

虽然构造函数能够简单的创建多个类似的对象,并且有了类的概念。但是我们发现每个方法都要在每个实例上创建一遍。

在ECMAScript中,函数也属于对象,即每创建一个函数都相当于实例化了一个对象。那上面的构造函数也可表示为:

function Person(name,age,sex){
    this.name=name;
    this.age=age;
    this.sex=sex;
    this.sayName=new Function("console.log(this.name)");
    //不难看出,每个Person实例都包含一个Function实例
}

可是,我们没有必要创建两个能够完成相同任务的函数,何况这个函数已经使用了this来绑定不同的实例。以sayName()函为例,我们只需要创建一个实例即可,谁调用这个函数,this就指向谁,就输出谁的名字。同样能完成任务,为什么要创建那么多的实例呢。

基于这种思想,后来就有了下面的方法:

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

function sayName(){
    console.log(this.name);
}

这样,我们只创建了一个sayName实例,达到了上面的那种效果。但这种方法也不好。

如果我们有多个像sayName这样的函数,那么我们岂不是要在全局作用域下声明那么多个函数,毫无封装性可言。

于是出现了下面的第三种方法。

3、原型模式

我们创建的每一个函数都有一个prototype的属性,该属性是一个指针,指向一个对象(原型对象),该对象包含了由特定类型的所有实例共享的属性和方法。           共享!!!!

这就意味着我们可以不用在构造函数中定义属性和方法了,直接定义一个空的构造函数,然后在构造函数外部给原型对象添加属性和方法

function Person(){}            

Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}

var p1=new Person();            //p1.name是"Rabbit"
var p2=new Person();            //p2.name也是"Rabbit"

理解原型对象:

创建一个函数(无论是函数声明还是函数表达式),就会生成一个prototype属性,该属性指向一个原型对象。默认情况下原型对象只会有一个constructor属性,该属性是一个指针,指向prototype属性所在的构造函数。当用构造函数创建了一个对象实例后,该实例内部将包含一个指针,指向构造函数的原型对象。在谷歌、火狐、safari浏览器中这个指针被叫做__proto__,可在控制台中输入一个对象实例来发现这个指针。这样,构造函数和原型对象之间通过prototype属性和constructor属性连接起来对象实例和原型对象之间通过__proto__属性连接起来而构造函数和对象实例之间没有直接的联系

isPrototypeOf( ) 确定对象实例和原型对象之间的关系

Object.getPrototypeOf( ) 获得某个对象实例的原型对象

console.log( Person.prototype.isPrototypeOf( p1 ) );           //返回true
var proto=Object.getPrototypeOf(p1);
console.log(proto.name);                    //返回“Rabbit”

每当代码读取对象实例的某个属性的时候,会先去对象实例里寻找是否有这个属性,如果有则返回,若没有则去对象实例的原型中去寻找。这就是对象实例共享原型中属性和方法的原理。

我们能够通过对象实例去访问原型对象中的属性,却不可以修改(重写)原型对象中的属性。例如:

function Person(){}            

Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}

var p1=new Person();
p1.name="Arch";     
console.log(p1.name);      //返回"Arch" 
var p2=new Person();            
console.log(p2.name);      //返回"Rabbit"

从上面的结果可见,p1.name=“Arch” 只是给p1这个实例设置了name属性,其原型中的name属性还在,只是被p1给屏蔽了而已,通过p2.name仍然能访问到原型中的name属性。所以说只能屏蔽,不能修改。即使将p1.name=null也不能恢复成原型对象的name属性,但是通过delete p1.name删除实例中的name属性来恢复至原型中的name属性。

hasOwnProperty( prop ) 查看属性在实例中还是在原型中,在实例中则返回true。p1.hasOwnProperty(name)  //返回false

in操作符:

console.log( “name” in p1);                        //只要name属性在p1对象中就会返回true,无论name是实例属性还是原型属性。

所以使用 in 和 hasOwnProperty( )  就能够确定属性是在实例中还是在原型中。 

for-in 可遍历所有可枚举的属性,包括实例属性和原型属性。

Object.keys( )  可获得对象上所有的可枚举实例属性。

Object.getOwnPropertyNames( )  获取对象上所有的实例属性,不管是否可枚举。

更为简单的原型写法:不用重复的写多次Person.prototype

function Person(){}

var old=Person.prototype;

Person.prototype={
    //constructor:Person,      如果想Person.prototype.constructor再次指向Person,只能强行加入
    name:"Rabbit",
    age:18,
    sex:"male",
    sayName:function(){alert(this.name);}
}

console.log(old.constructor);                //指向Person
console.log(Person.prototype.constructor);   //指向Object

但是,这相当于重写了原型对象。于是就是失去了构造函数和原型对象之间的关系。Person的原型对象依然存在,其中的constructor属性仍然指向Person的构造函数。只是Person.prototype不再指向Person了。新的原型对象依然有constructor属性,不过它指向Object构造函数。新的原型对象实际上就是Object对象的一个实例,所以新的原型对象的constructor指向Object。

强行指向的constructor属性的 [[Enumerable]] 特性 会被设置为true,可以使用Object.defineProperty( ) 修改回去。

原型的动态性:

function Person(){}            

Person.prototype.name="Rabbit";
Person.prototype.age="18";
Person.prototype.sex="male";
Person.prototype.sayName=function(){alert(this.name);}

var p1=new Person();
Person.prototype.sayHi=function(){
    alert("Hi!");
}
p1.sayHi();            //弹出"Hi!"

当p1调用sayHi()函数时,会先在实例中寻找这个函数,找不到才回去原型中找,在原型中找到即调用。

然而如果像上面那样重写了原型对象:

function Person(){}

var p1=new Person();
Person.prototype={
    name:"Rabbit",
    age:18,
    sex:"male",
    sayName:function(){alert(this.name);}
}
p1.sayName();        //报错

因为p1的__proto__属性仍然指向原来的原型对象,原来的原型对象并没有sayName()函数。

原型对象的问题:

原型最大的特点就是它能够共享,但这也是它的缺点。共享对于那些需要共享的函数来说确实很方便,但是一个对象总有些不用共享的属性和函数。对于基本类型的属性来说,这种共享还没太大的问题。p1.name="aaa",是在设置p1的实例属性,不会影响到p2的name值。但是对于那些引用类型的属性来说就显得比较麻烦了。

function Person(){}

var p1=new Person();
Person.prototype={
    constructor:Person,
    name:"Rabbit",
    age:18,
    sex:"male",
    friends:["zs","ls"],
    sayName:function(){alert(this.name);}
}

var p1=new Person();
var p2=new Person();
p1.friends.push("ww");
console.log(p1.friends);        //["zs","ls","ww"]
console.log(p2.friends);        //["zs","ls","ww"]

每个人都有不同的friends,但事实却是这个属性被共享了,并且p1的修改会影响到p2。

一般的实例都会有自己的实例属性,这些属性是不能共享的。所以单独使用原型模式达不到这种效果。

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

在构造函数中定义实例属性和方法,在原型中定义共享的属性和方法。

function Person(name,age,sex,friends){
    this.name=name;
    this.age=age;
    this.sex=sex;
    this.friends=friends;
}
Person.prototype={
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
}

这样,每个实例既有自己独立的实例属性,又有共享的函数方法。

可能会有人觉得这么分开写会有些奇怪,于是又出现了动态原型模式

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

    if(typeof Person.prototype.sayName != "function" ){            //也可以写成this.sayName
        Person.prototype.sayName=function(){alert(this.name);}
        Person.prototype.sayHi=function(){alert("Hi");}
        .....
    }
}

在创建第一个对象实例时,就将原型方法创建好,以后再创建其他对象实例的时候就不用创建了。

这样结合了构造函数和原型模式的优点,而且看起来也不会太怪。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值