Javascript高级程序设计读书笔记——面向对象的程序设计(下)

ECMAScript中的对象其实就是键值对,值可以是数据或函数。每个对象都是基于一个引用类型创建的,可以是之间提到的原生类型,也可以是自定义类型。

理解对象

属性类型

数据属性

数据属性包含一个数据值的位置,它包含四个特性:
1. [[Configurable]]: 能否通过delete删除属性,是否可以修改属性特性
2. [[Enumerable]]: 能否通过for-in循环返回该属性
3. [[Writable]]:能否修改属性值
4. [[Value]]:包含此属性的数据值,无论读取或者写入属性值都发生在这个位置
举一个例子:

var person= {
  name: "Nicholas"
}

name属性的[[Configurable]] [[Enumerable]] [[Writable]]均为true[[Value]]为”Nicholas”。
如何修改数据属性的特性呢?
我们可以使用Object.defineProperty(),此方法接收三个参数:属性所在的对象、属性的名字、一个描述符对象(描述符对象只能包含configurable enumerable writable value四个属性中的0个或多个。比如:

Object.defineProperty(person, "name", {
    writable: false,
    value: "Sue"
})
//{name: "Sue"}
访问器属性

访问器属性不包含数据值,包含一对gettersetter,读取该属性时,调用getter,写入该属性时,调用setter,它有4个特性:
1. [[Configurable]]:能否通过for-in循环返回该属性,是否可以修改属性特性
2. [[Enumerable]]:能否通过for-in循环返回该属性
3. [[Get]]:读取该属性时调用的函数,默认为undefined
4. [[Set]]:写入该属性时调用的函数,默认为undefined
访问器属性只能通过Object.defineProperty()定义。在实际开发过程中,我们经常需要为对象内的私有属性定义一个相应的访问器属性,私有属性通常使用_标记,举个例子:

var person= {
  name: "Nicholas",
  _age: 24
}
Object.defineProperty(person, "age", {
    get: function() {
        return this._age-6
    },
    set: function(age) {
        this._age = age
    }
})

person.age=23
person.age
//17

如果一个访问器属性只定义了getter,表示这个属性时只读的,如果只定义了setter,那么这个属性只可写入不可读取,比如:

var person = {
    _secret: "", 
    _name: "Sue"
}
Object.defineProperty(person, "name", {
    get: function() {
        return this._name
    }
})
Object.defineProperty(person, "secret", {
    set: function(secret) {
        this._secret = secret;
    }
})
person.secret; //undefined
person.name="Jane"; 
person.name; //"Sue"

defineProperty()是ECMAScript的一个方法,支持这个方法的浏览器有IE 9+、Firefox 4+、Safari 5+、Opera 12+、Chrome,在不支持这个方法的浏览器中,如果要创建访问器属性,我们可以尝试__defineGetter__ __defineSetter__,比如:

person.__defineGetter__("firstName", function() {
    return "Zhao"
})
person.firstName
//"Zhao"

定义多个属性

可以使用defineProperties()定义多个属性,举个例子:

var person = {}
Object.defineProperties(person, {
  _name: {
    value: "Sue"
  },
  _secret: {
    value: "No"
  },
  name: {
    get: function() {
      return this._name
    }
  },
  secret: {
    set: function(secret) {
      this._secret = secret
    }
  }
 })

支持Object.defineProperties()的浏览器与Object.defineProperty()相同。

读取属性的特性

使用Object.getOwnPropertyDescriptor()接收两个参数:属性所在的对象、需要读取的属性,返回一个对象。如果要读取的属性是数据属性,则返回的对象包含的属性为:configurable enumerable writable value,如果该属性是访问器属性,则返回的对象包含的属性有:configurable enumerable get set

Object.getOwnPropertyDescriptor(person, "name")
//{get: ƒ, set: undefined, enumerable: false, configurable: false}
Object.getOwnPropertyDescriptor(person, "_name")
//{value: "Sue", writable: false, enumerable: false, configurable: false}

支持Object.getOwnPropertyDescriptor()的浏览器与Object.defineProperty()相同。

创建对象

对象字面量

之前提到过的,创建对象比较简单的方法,举个例子:

var person = {  
  name: "Sue"
};

但这种方式不可重用。

工厂模式

当我们要创建多个相似对象时,我们可以使用工厂模式,举个例子:

function createPerson(name, secret){
    var person = {};
    person.name = name;
    person.secret = secret;
    person.makeFriends = function() {
        return "Hello, I am " + this.name;
    };
    return person;
}

通过这种方式,我们可以方便的创建多个person对象,这些对象均拥有name secret makeFriends属性,在很多情况下,这样写基本上可以满足我们的需求,美中不足的是对象与对象之间并不知道它们是”相似”的。

构造函数模式

将上面的例子改写如下:

function Person(name, secret) {
    this.name = name;
    this.secret = secret;
    this.makeFriends = function() {
        return "Hello, I am " + this.name;
    }
}

上面的例子与工厂模式有以下几点不同:
1. 没有显式的创建对象
2. 属性和方法赋值给this而非方法内创建的对象
3. 没有return
4. 函数名首字母大写。为了区别于非构造函数,按照其他OO语言的写法,我们最好将构造函数名的首字母大写。
5. 创建对象的方式不同:

var p1 = createPerson("Sue", "No");
var p2 = new Person("Sue", "No");

需要注意的是,要创建Person实例,需要使用new操作符,var p2 = new Person("Sue", "No")会执行以下4步:
1. 创建新对象{}
2. 使this指向新对象
3. 执行构造函数中的代码
4. 返回新对象

假如我们不使用new操作符:

var p3 = Person("Sue", "No")
p3; //undefined
window.name; //"Sue"

不使用new操作符时,就是一次普通的函数调用,并且将返回值赋给p3,由于没有返回值,因此为undefinedPerson()方法调用时,作用域指向window,因此name等属性直接添加到了window对象中。
如何在不使用new操作符时,将name等属性添加到指定的对象上呢?

var p3 = {};
Person.call(p3, "Sue", "No");
p3.name; //"Sue"

我们已经对构造函数模式有了大致的了解,那么这个模式能否解决之前提到的“相似度”问题呢?
答案是可以的,我们可以使用两种方式来判断:
1. constructor

var p1 = new Person("Sue", "No")
var p2 = new Person("Jane", "No")
p1.constructor === p2.constructor
//true
  1. instanceof
p1 instanceof Person
//true
p2 instanceof Person
//true

Ok,现在看来我们的问题已得到解决…但新的问题又来了…
每一个Person的实例对象都拥有一个makeFriends方法,它们都做着同一件事情,却被实例化了很多次,这显然是没有必要的事情。为了解决这个问题,我们引入了原型模式。

原型模式

理解原型对象

创建新函数时,会为函数添加一个prototype属性,prototype属性亦会包含一个constructor属性,该属性指向函数本身,当从构造函数创建一个新实例时,实例会通过某种方式与这个原型对象连接(Firefox、Chrome、Safari中实例对象的__proto__属性指向构造函数的prototype属性),并获得包含在原型对象中的属性和方法。
在下面这段代码中,我们创建Person构造函数,然后为它的原型对象添加方法,最后创建一个Person的实例person

function Person() {
}
Person.prototype.makefriends = function() {
    return "no";
}
var person = new Person();
person.makefriends(); // "no"

Person构造函数、person实例之间有如下关系:

那么我们要怎么判断这种关系呢?
1. 从上图中可以看出,我们首先可以通过constructor属性来判断:

person.constructor === Person
// true
  1. 使用isPrototypeOf()
    isPrototypeOf可以判断实例对象的[[prototype]]是否指向调用这个方法的对象(某个构造函数的prototype属性)。
Person.prototype.isPrototypeOf(person)
// true
  1. 使用Object.getPrototypeOf()
    Object.getPrototypeOf()是ECAMScript 5 新添加的一个方法,可以获得一个实例对象的[[prototype]]属性。
Object.getPrototypeOf(person) === Person.prototype
// true

ECMAScript引入原型的初衷是为了实现简易的继承,实例通过原型获得了构造函数的公共属性和方法,当我们访问实例中的某个属性或方法时,首先是去实例对象中找,如果找不到才去构造函数的原型中去找,给实例对象中的某个属性或方法赋值,并不会影响构造函数的原型中的同名属性或方法。举一个例子:

var p1 = new Person()
p1.makefriends
//ƒ () {
//    return "no";
//}
var p2 = new Person()
p2.makefriends
//ƒ () {
//    return "no";
//}
p1.makefriends = function() {};
p1.makefriends
//ƒ () {}
p2.makefriends
//ƒ () {
//    return "no";
//}

那么p1.makefriends如何回归原型中的值呢?可以使用delete将实例对象中的makefriends方法删除

delete p1.makefriends
true
p1.makefriends
ƒ () {
    return "no";
}
属性的存在性判断
  1. in操作符
    in操作符可以判断是否可以访问某个属性,无论此属性存在于实例对象中还是构造函数的原型对象中,都会返回true
p1.name = "Sue";
"name" in p1
// true
"makefriends" in p2
// true
  1. hasOwnProperty()方法
    我们如何才能知道一个属性存在于实例对象还是构造函数的原型?可以使用hasOwnProperty(),只有当属性是在实例对象中定义时,返回true
p1.name = "Sue";
p1.hasOwnProperty("name");
// true
p2.hasOwnProperty("makefriends");
// false

既然hasOwnProperty()只可以判断属性是否存在实例对象中,那么如何判断属性是否只存在于构造函数的原型中呢,书中给了一个例子:

function hasOwnPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
hasOwnPrototypeProperty(p2, "makefriends");
// true
hasOwnPrototypeProperty(p1, "name");
// false
p1.makefriends = function(){}
hasOwnPrototypeProperty(p1, "makefriends");
// false

注意最后一个例子,如果实例对象中的属性覆盖了构造函数原型中的属性,那么当判断此属性是否存在于实例对象中时返回true,因此hasOwnPrototypeProperty()返回false

遍历属性
  1. for-in
    使用for-in循环可以返回实例对象中和构造函数原型对象中所有可枚举的属性(即属性的enumerable特性设置为true
var p1 = new Person();
p1.name = {
  value: "Sue",
  enumerable: false
};
p1.makefriends = function() {}
for (var prop in p1) {
  console.log(prop)
}
// name
// makefriends
  1. Object.keys()
    Object.keys()是ECMAScript新添加的方法,可以返回对象上可枚举的实例属性数组。
Object.keys(Person.prototype);
// ["makefriends"]
var person = new Person();
Object.keys(person);
// []
person.name = "Sue";
Object.keys(person);
// ["name"]
  1. Object.getOwnPropertyNames()
    Object.getOwnPropertyNames()是ECMAScript新添加的方法,可以返回所有实例属性,包括不可枚举的(比如constructor属性)
Object.getOwnPropertyNames(Person.prototype)
// (2) ["constructor", "makefriends"]
封装原型对象

如果我们要重写整个原型对象,并添加多个属性和方法,我们可以使用对象字面量的形式:

Person.prototype = {
    makefriends: function() {
        return "No"
    }
}

这种写法不仅简便,而且也体现了封装性,但有一个问题:

Person.prototype.constructor
// ƒ Object() { [native code] }

重写整个原型对象会改变Person.prototype.constructor,不利于我们通过这个属性来判断实例与构造函数之间的关系,因此我们在重写时要手动添加constructor属性:

Person.prototype = {
    makefriends: function() {
        return "No"
    },
    constructor: Person
}
Person.prototype.constructor
//ƒ Person() {
//}

然而这样写仍有一个问题,创建函数时自动添加的constructor属性是不可枚举的,而我们手动添加的默认是可枚举的,因此将上述例子修改为:

Person.prototype = {
    makefriends: function() {
        return "No"
    }
}
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
})

这样修改后仍然有一个问题,重写原型对象后,构造函数的原型就会指向一个新的对象,而如果重写之前此构造函数已有实例,那么这个实例的[[prototype]]仍然指向之前的原型对象,导致对原型的修改对此实例失效,看一个例子:

function Person() {
}
var person = new Person();
Person.prototype = {
    constructor: Person,
    name: "Sue"
};
person.name;
// undefined

此时Person构造函数 person实例之间的关系如下图:

因此使用对象字面量重写原型最好是在创建构造函数的时候。

原生对象的原型

所有原生的引用类型,都是采用原型创建的,它们的构造函数原型上都定义了方法:

Array.prototype.concat
// ƒ concat() { [native code] }

我们也可以在原型对象的原型中添加自定义的方法:

String.prototype.defaultName = function() {
    return "Sue";
}
var name = "Jane";
name.defaultName()
//"Sue"
原型模式的问题

原型中所有的属性都是在实例间共享的,实例不能保管它们的私有对象,这显然不符合面向对象的思想。举一个例子:

function Person() {}
Person.prototype.hobbies = [];
var p1 = new P`hasOwnProperty()`erson();
p1.hobbies.push("swimming");
p1.hobbies;
//["swimming"]
var p2 = new Person();
p2.hobbies.push("dancing");
p2.hobbies;
//(2) ["swimming", "dancing"]

构造函数模式+原型模式

这是应用比较广泛的一种模式,构造函数中可以定义每个对象特有的属性,原型中可以定义对象共有的属性和方法,解决了上文中提到的问题。比如:

function Person(name, secret) {
    this.name = name;
    this.secret = secret;
}
Person.prototype.makefriends = function() {
    return this.secret;
}
var p = new Person("Sue", "Null")
p.makefriends()
// "Null"
p.constructor
//ƒ Person(name, secret) {
//    this.name = name;
//    this.secret = secret;
//}

动态原型模式

在上一节中,我们虽然实现了私有的属性+共享的方法,但构造函数和原型之间仍不具有封装性,我们单独创建了构造函数又单独修改了原型对象,那么如果我们把它们封装在一起呢?

function Person(name, secret) {
    this.name = name;
    this.secret = secret;
    Person.prototype.makefriends = function() {
        return this.secret;
    }
}

那么每次实例一个对象,都要修改一次原型对象,这显然过于冗余,跟工厂模式非常相似。那有没有一种方式可以让原型对象是修改一次呢?答案自然是有的…

function Person(name, secret) {
    this.name = name;
    this.secret = secret;
    if(typeof Person.prototype.makefriends != "function")
      Person.prototype.makefriends = function() {
          return this.secret;
      }
      Person.prototype.sayHi = function(){
          return "Hi, I am " + this.name;   
      }     
}

通过判断原型对象中是否存在某个方法来判断是否已经存在一系列方法(没必要每个方法都判断),堪称完美。

寄生构造函数模式

这个方式很像工厂模式,区别在于创建对象时需使用new操作符。

function Person(name, secret) {
    var o = new Object();
    o.name = name;
    o.secret = secret;
    o.makefriends = function() {
        return o.secret;
    }
}
var person = new Person("Sue", "No")
person.constructor === Person
// true

当我们需要在原生引用类型的基础上添加更多功能,同时又不想更改原生引用类型时,可以使用这种方式,比如书中提到的例子:

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", "green");
alert(colors.toPipedString()); //"red|blue|green"
colors.constructor === SpecialArray
// false

这里笔者遇到了一个问题,书中提到寄生构造函数模式创建的实例与构造函数没有关系,笔者在上面两个例子中都做了验证,第一个显示是有关系的,第二个则显示没有关系,大家如果知道为什么希望告知…

稳妥构造函数模式

Douglas Crockford发明了durable objects这个概念,稳妥对象没有公共属性,其方法也不引用this对象,其创建对象的方式与最初介绍的对象字面量的方式相似。

function Person(name, secret) {
  var p = new Object();
  p.sayName= function() {
    return name;
  };
  return p;
}
var person = Person("Sue", "No");
person.sayName();
// "Sue"

person中保存了一个稳妥对象,我们只能通过调用sayName()来访问name属性。

稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环
境——例如,ADsafe(www.adsafe.org)和Caja(http://code.google.com/p/google-caja/)提供的环境——
下使用。

猜你喜欢

转载自blog.csdn.net/weixin_40264719/article/details/79452307