最详细的JavaScript高级教程(十六)创建对象

创建一个对象再给这个对象赋值的操作需要大量的代码,如果要创建多个对象,就要写很多重复代码,对象的创建可以使用下面这些方法来避免写大量的不好维护的重复代码。

工厂模式

  • 优点:创建一个对象的大量实例
  • 缺点:无法进行对象识别,即使用工厂模式创建的对象,还是Object对象,不是一种新的对象,也就不能使用instanceof进行验证。总结就是说:工厂模式虽然创建了Person类的实例,但是却没有创建Person本身,Person的实例也无法标识出来。
function createPerson(name, age){
    var o = new Object();
    o.age = age;
    o.name = name;
    o.toString = function(){
        return name + ' ' + age;
    }
    return o; // 注意工厂方法要把o给return出去
}
var person1 = createPerson('w', 12);
alert(person1.toString()); // w 12

使用构造函数

使用构造函数方法可以完美的解决工厂方法的问题,它可以创建一种新的类型,并且允许我们使用这个类型创建实例。

先看一下构造函数方法的写法,它的写法与函数十分类似:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    }
}
var person1 = new Person('Nic', 29);
var person2 = new Person('Greg', 12);
person1.sayName(); //Nic

我们需要注意:

  • 构造函数的命名,首字母应该大写,以标识这是一个构造函数
  • 不用显示的创建对象,在构造函数中我们不需要写new Object
  • 直接将属性和方法赋值给this对象
  • 不用return
  • 创建实例的时候,使用new关键字

当我们这么创建了对象之后,就可以使用instanceof来看对象不是是属于一个类型:

alert(person1 instanceof Person); // true
alert(person2 instanceof Person); // true
var s = {};
alert(s instanceof Person); // false

在对象中,还保存着构造函数的实例,我们通过constructor属性可以看到一个对象是不是实现了某个构造函数。这个方法虽然可以使用,但是一般来说使用instanceof更好。

var s = {};
alert(person1.constructor == Person); //true
alert(s.constructor == Person); //false

js有意思之处就是,虽然它是构造函数,但是使用了函数的语法,所以,这个方法一定能当作一个普通的函数使用。我们看构造函数,它的函数体中写的都是this. 表示构造函数其实是在自己的作用域中添加属性和方法。那么:

  • 如果在全局对象中直接调用一个构造函数,则属性和方法被添加到全局作用域中
  • 如果使用apply方法在一个对象的作用域中调用构造函数则这些属性和方法被添加到调用作用域中
function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    }
}
// 将构造函数作为普通函数调用
Person('wh', 15);
alert(window.name); // 严格模式下报错,非严格模式下返回wh
// 在另一个对象的作用域中使用
var o = new Object();
Person.apply(o, ['ww', 12]); // 或者使用Person.call(o, 'ww', 12)
o.sayName(); //ww

构造函数虽然好,但是也有无法解决的问题,就是在构造中定义函数的时候,每一个实例对象,都会在自己的内存控件定义一份该方法的副本。而事实上,方法只是一个解决问题的办法,定义这么多是不应该的。我们可以通过定义全局方法,然后给其赋值的办法,如下:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var person1 = new Person('Nic', 29);
person1.sayName(); //Nic

这个方法可以解决这个问题,但是我们发现定义了很多全局的方法,这样不仅会将增加全局代码的复杂度,同时也打乱了作用域,让代码可读性变差。所以这个问题,我们还需要下面介绍的原型模式来解决。

原型模式

每一个函数都具有一个prototype属性。(注意是每一个函数,而不是每一个构造函数,构造函数本质上也是函数)

这个prototype属性保存了所有实例共享的属性和方法。我们可以认为一旦一个属性写入了一个函数的prototype中,则这个属性被全部实例共有。

如果需要访问原型对象,有下面两种方法:

  • 使用构造函数的prototype属性
  • 使用实例的__proto__属性
function Person() {}
Person.prototype.name = 'jo';
var person1 = new Person();
alert(person1.name); //jo
alert(person1.__proto__.name); //jo

比较好理解的是,一个方法一旦写入了函数的prototype中,则所有实例共有这个方法:

function Person(){
    this.__proto__.say = function(){
        alert('hello');
    };
}
var person1 = new Person();
var person2 = new Person();
person1.say(); //hello
person2.say(); //hello

而如果原型中定义了一个属性,大家都公用,那不是所有的实例都一样了么?其实不是,所有的实例拥有原型所有的原型对象上的属性和方法,而他们自己允许重写这些方法和属性,当我们访问一个实例上的属性的时候,解析器会先去寻找有没有复写这个属性,如果有,用复写的方法,如果没有,使用原型对象的方法,下面一个例子说明了这种查找值的做法:

function Person() {}
Person.prototype.name = 'jo';
var person1 = new Person();
alert(person1.name); //jo ---没有复写,使用原型对象的值
alert(person1.__proto__.name); //jo
person1.name = 'mo';
alert(person1.name); //mo   --- 复写过了,来源于复写的值
alert(person1.__proto__.name); //jo ---原型对象上值不变

下面这张图有助于我们理解其覆盖关系:
在这里插入图片描述

原型模式的高级用法

  • constructor 在构造函数中,prototype指向了原型对象,在原型对象中constructor指针指向了构造函数,他们互相指,提高了灵活性,如下图
    在这里插入图片描述
    Person.prototype ---原型对象
    Person.prototype.constructor ---指向构造函数
    
  • 判断实例的原型是否是某一个对象的方法
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    var b = Person.prototype.isPrototypeOf(person1); // true
    
  • 从实例获取原型对象,可以使用__proto__也可以使用Object.getPrototypeOf
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    var b = Object.getPrototypeOf(person1) == person1.__proto__; // true
    
  • 删除实例上的属性和方法(只能删除实例的,不会影响原型上的)
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    person1.name = 'p';
    alert(person1.name); // p
    delete person1.name;
    alert(person1.name); // jo 原型对象上的属性仍在
    
  • 判断一个属性属于实例还是属于原型对象:hasOwnProperty(访问的是实例上的返回true,访问的是原型上的返回false)
    function Person() {}
    Person.prototype.name = 'jo';
    var person1 = new Person();
    person1.name = 'p';
    alert(person1.name); // p
    alert(person1.hasOwnProperty('name')); // true
    delete person1.name;
    alert(person1.name); // jo
    alert(person1.hasOwnProperty('name')); // false
    
  • 判断一个属性是否在实例上或者原型上:in(只要能访问到,就返回true)
    function Person() {}
    var person1 = new Person();
    Person.prototype.name = 'jo';
    alert('name' in person1); //true
    person1.name = 'w';
    alert('name' in person1); //true
    
  • 判断一个属性是否仅存在于原型上
    // 如果一个属性仅存在于原型上返回true
    function hasPrototypeProperty(object, name){
        return !object.hasOwnProperty(name) && (name in object);
    }
    
  • 给原生对象添加新的方法
    // 可以给String添加方法,很简单,就是给其原型添加方法
    String.prototype.startsWith = function(text) {
        return this.indexOf(text) == 0;
    };
    var msg = 'hello';
    alert(msg.startsWith('h'));
    
    虽然可以这么做,但是我们不建议这么做,因为这样可能导致命名冲突,也可能意外的重写原生方法。

简单的原型语法

在上一课中我们遍历了Person的prototype中所有的属性,我们发现除了我们定义的属性,只有一个constructor属性,指向其构造函数,这个我们在这一课的学习中也看到了相应的说明。

在定义一个构造函数的prototype的时候,需要一遍一遍的写Person.prototype,这样很繁琐,我们可以使用一个新的对象来代替prototype来达到快速构建原型对象的目的。

  function Person() {}
  // 直接构建其原型数组
  Person.prototype = {
    name: '23',
    age: 1
  };
  var p2 = new Person();
  alert(p2.name); //23
  alert(Person.prototype.constructor); //constructor没有定义,根据之前讲的查找顺序指向了Object的构造函数

我们发现,新构建的原型没有constructor,而constructor应该指向Person方法,这时候如果我们在之后的逻辑中用到了constructor,则需要重新构建constructor,如果用不到,不管也行。

需要注意的是,原先的constructor是不可枚举的,我们可以通过简单的赋值操作来给constructor赋值,这样赋值的constructor是可以枚举的,如果我们要构建与原来一模一样的constructor,需要使用我们之前学过的defineProperty方法

// 直接恢复原型中的constructor
Person.prototype = {
    constructor: Person,
    name: '23',
    age: 1
  };
// 构建不可枚举的constructor
function Person() {}
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
});

直接使用一个对象来定义构造函数的prototype还有一个潜在的风险,就是如果实例化在对象赋值给prototype之前,这个对象的prototype指向会错误。我们在验证这个知识的时候需要知道下面几个知识:

  • 实例和构造函数之间没有联系,他们俩是通过prototype来联系在一起的,实例中有指针指向prototype,prototype和构造函数互相有指向
  • 实例prototype的确定是在实例化的时候(new的时候)
function Person() {}
// 先实例化,此时p2中原型对象指向Person的原型
var p2 = new Person();
// 直接构建其原型数组,prototype的指向被更改,p2中的prototype失效
Person.prototype = {
    name: '23',
    age: 1
};
alert(p2.name); //undefined

用下面的图也可以解释这种现象在这里插入图片描述

原型模式的缺点

我们说原型模式用于在各个实例中共享一些属性,它强于共享方法,甚至我们提出原型模式就是为了解决共享方法的问题。

当我们提到了共享属性,接受了原型模式会共享属性的时候,我们发现这个特性用处并不大,因为一般来说,实例需要有自己的属性,原型属性当个默认值尚且合格。

而我们之前在做例子的时候使用的属性都是值类型的,当我们使用引用类型的属性的时候,我们就发现了一个很蛋疼的现象,修改了其中一个引用类型的值,另一个也跟着变了

function Person() {}
Person.prototype.arr = [1, 2];
var p1 = new Person();
var p2 = new Person();
p1.arr.push(3);
alert(p2.arr); //1,2,3

虽然我们能理解这种在引用属性上面发生的异常情况,但是这通常不是我们想要的。那么怎么解决这种问题呢?我们想想之前讲的两种模式,工厂模式和构造函数模式,他们两个都可以解决在初始化的时候给对象赋值的操作,而构造函数方法更加的方便快捷,我们通过构造函数方和原型方法的结合,就可以最舒服的完成对象的创建。

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

有了我们之前的积累,我们发现,结合构造函数模式和原型模式可以解决我们的问题,事实上,这也是我们创建自定义类型最常见的方式。我们要达到的效果:

  • 共享的属性和方法可以共享
  • 不共享的属性每个实例都有一份自己的副本

要实现这种“智能”的创建对象,也十分容易:

  • 在原型对象中构建共享的属性和方法
  • 在构造函数中构建各自的属性
  function Person(name, job) {
    this.name = name;
    this.job = job;
    // 每个实例都拥有一个friends数组,下面的方法等于设置了默认值
    this.friends = ['Shel', 'Cour'];
  }
  Person.prototype = {
    constructor: Person,
    sayName: function() {
      alert(this.name);
    }
  };
  var p1 = new Person('Nic', 'SE');
  var p2 = new Person('Greg', 'Doctor');

  p1.friends.push('Van');
  alert(p1.friends); //Shel,Cour,Van
  alert(p2.friends); //Shel,Cour
  alert(p1.sayName == p2.sayName); //true

组合构造模式解决了之前提到的问题,但是,构造函数和原型对象分开的写法既不好理解也带来了额外的理解成本,所以我们也可以在构造函数中初始化原型。下面介绍的动态原型模式可以解决这个问题。

动态原型模式(推荐)

这是最好的定义对象的实践,我们推荐使用这种方法。注意其中方法的初始化,判断方法是否存在以期只初始化一次方法。

function Person(name, age){
    // 初始化属性
    this.name = name;
    this.age = age;
    // 这样写原型只会初始化一次,是比较好的方法
    // 如果有好几个函数,不需要一个一个的判断,可以只判断其中一个,目的就是看看是不是第一次实例化
    if(typeof this.sayName != 'function'){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

寄生构造函数模式(不常用)

寄生模式是为了解决之前提到的:最好不要给基本类型的原型添加方法,容易造成命名冲突的问题。如果我确实想给Array添加一个方法,就最好用寄生模式。

在正常情况下,不推荐使用这个模式,只有在上面提到的特定的情况之下使用才比较好。

寄生模式的定义与工厂模式一摸一样,只是在使用的时候,用new来进行初始化,其初始化的对象也无法解决工厂模式的问题,即也存在对象无法识别的问题,所以这种模式只是工厂模式的变体,实际使用的时候尽量不用。

function SpecialArray(){
    var values = new Array();
    values.push.apply(values, arguments);
    values.toPipedString = function(){
        return this.join('|');
    }
    return values;
}
var colors = new SpecialArray('red', 'blue');
alert(colors.toPipedString()); //red|blue

稳妥构造函数模式(不常用)

稳妥构造模式是在原来工厂模式的基础上,去掉了公共的属性,只保留方法。而构造函数中定义的变量是私有变量,不暴露出来,这样,构造函数保护了所有的内部变量,只通过函数将需要进行的操作暴露出去,构建了一个安全的环境。

这种方式同样无法进行对象识别。与寄生一样,在特地情况使用。

function Person(name){
    var o = new Object();
    var name = name; // var定义的name,没有写 o.name = name; name没有被暴露出去,保护了局部变量
    o.toString = function(){
        return name;
    }
    return o;
}
var p = Person('wh');
alert(p.toString()); 'wh'
alert(name); //''
alert(p.name); // undefined
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值