JavaScript权威指南学习笔记(三)面向对象编程

目标

  • 理解对象属性
  • 理解并创建对象
  • 理解继承

ECMA-262对对象是这么定义的,“unordered collection of properties each of which contains a primitive value, object, or function.”,是一种包含一个初始类型值,对象或者函数的属性的集合。严格的说,对象是一种由无序数值组成的数组。每个属性和方法都通过一个名字绑定(mapped)到一个数值上。所以也可以通过哈希表来理解对象,如由一组键值对组成的,而且值可能为data或者function。

理解对象

两种构造方式:

//方式一:
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
    alert(this.name);
};
//方式二:
var person = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
        alert(this.name);
    }
};

属性的类型

ECMA-262 fifth edition describes characteristics of properties through the use of internal-only attributes. These attributes are defined by the specification for implementation in JavaScript engines, and as such, these attributes are not directly accessible in JavaScript. To indicate that an attribute is internal, surround the attribute name with two pairs of square brackets, such as [[Enumerable]]. Although ECMA-262 third edition had different definitions, this book refers only to the fifth edition descriptions.

ECMA-262 第五版通过只能在内部的特性的使用来描述属性的特征。这些特性在JavaScript引擎中通过对实现的说明来定义,这些特性在JavaScript中是无法直接访问的。为了让人区分出某个特性是内部的,给特性名加上两个中括号,如[[Enumerable]]。

第一个属性:

Data 属性

Data 属性为data值保存了一个单独的位置。Values都从这个位置读取,Data属性用四个特性(attributes)来描述它们的行为:

  • [[Configurable]] —— 表示这个属性通过 delete 删除它、change 它的特性或者将它改变成 accessor 属性这三种方法是否会被重定义。默认情况下,对于所有定义在对象上的所有属性(defined on the object)情况都为真。
  • [[Enumerable]] —— 表示这个属性在for循环中是否会返回。默认情况下,对于所有定义在对象上的所有属性(defined on the object)都会返回true,
  • [[Writable]] —— 表示这个属性是否可变,默认情况下,对于所有定义在对象上的所有属性(defined on the object)情况都为真。
  • [[Writable]] —— 保存对象的实际值,这是属性值读取和新值保存的位置。该特性的默认值是undefined。

当一个对象添加一个属性时,属性的[[Configurable]]、[[Enumerable]]和[[Writable]]都被设置成true,同时[[Value]]被设置成被赋的值。如

var person = {
    name: "Nicholas"
};

属性为 name,被赋值为”Nicholas”。意味着[[Value]]被赋值为”Nicholas”,任何对这个值得改变都储存在这个位置。

改变这些默认属性,必须使用ES5的 Object.defineProperty() 方法。这个方法接受三个参数,被执行对象,要改变的属性名和一个描述性对象,这个描述性对象的属性匹配这些特性 configurable, enumerable, writable, 和 value,如:

var person = {};
Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas"
});
alert(person.name); //"Nicholas"
person.name = "Greg";//在严格模式下,这种赋值会报错
alert(person.name); //"Nicholas",注意writable为false

类似的,配置一个nonconfigurable属性:

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Nicholas"
});
alert(person.name); //"Nicholas"
delete person.name; //严格模式会报错
alert(person.name); //"Nicholas"

另外,一旦一个属性被配置成nonconfigrable,它就不能再被配置成configruable。任何尝试调用Object.defineProperty()和改变除了writable意外其它特性的尝试都会报错:

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Nicholas"
});
//throws an error
Object.defineProperty(person, "name", {
    configurable: true,
    value: "Nicholas"
});

当你使用Object.defineProperty()时,configurable, enumerable, 和
writable的默认值都是false,除非你特别指明。一般不会用到,只是为了理解JavaScript对象。

第二个属性:

Accessor 属性

支持浏览器:Internet Explorer 9+ (Internet Explorer 8部分支持), Firefox 4+, Safari 5+, Opera 12+, 和 Chrome

Accessor属性不包含数值,它是getter和setter的结合(尽管非必要)

  • [[Configurable]] —— 表示这个属性通过 delete 删除它、change 它的特性或者将它改变成 accessor 属性这三种方法是否会被重定义。默认情况下,对于所有定义在对象上的所有属性(defined on the object)情况都为真。
  • [[Enumerable]] —— 表示这个属性在for循环中是否会返回。默认情况下,对于所有定义在对象上的所有属性(defined on the object)都会返回true,
  • [[Get]] —— 读取属性时调用,默认值是undefined。
  • [[Set]] —— 写的时候调用,默认值是undefined。

无法显示定义 accessor 属性。必须通过 Object.defineProperty()。

var book = {
    _year: 2004,//一般加下划线,表示不想被对象方法之外的函数调用
    edition: 1
};
//定义year为一个accessor属性,通过getter获取_year值,
//通过setter获取正确的edition。
Object.defineProperty(book, "year", {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});
book.year = 2005;
alert(book.edition); //2

getter和setter没必要全设,只给getter赋值代表不能写入,任何尝试写入都会被忽略,严格模式下,甚至会报错,同理,只给setter赋值代表任何读取操作会返回undefined,严格模式下,同样会报错。

定义多重属性

支持浏览器:Internet Explorer 9+, Firefox 4+, Safari5+, Opera 12+和 Chrome。

//同时创建了多个属性,这段代码等同于上面的代码,
//唯一不同的是同时创建多个属性。
var book = {};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

读取属性的特性

支持范围:JavaScript任何对象,包括DOM和BOM对象,Internet Explorer 9+, Firefox 4+,Safari 5+, Opera 12+, 和 Chrome.

var book = {};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.confi gurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"

对象构造

工厂方法,利用同一接口构造一堆对象而产生一堆代码拷贝。

工厂模式

作用:抽象化对象的构造过程
因为没有classes,开发者利用函数封装构造对象的接口,如下:

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式并不强调对象的类型。

#

构造模式
上一章讲了原生构造函数如Object和Array

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

Person()和createPerson()中的代码大部分相同,除了以下几点:
* 没有对象显式创建
* 属性和方法直接创建在this对象上
* 没有返回语句
* 构造函数首字母大写,普通函数首字母小写
创建一个新的Person实例,使用new操作符。调用构造函数引发下面四步:

  1. 创建一个新对象
  2. 构造函数的this值指向新对象。
  3. 执行构造函数内部代码(给新对象添加新属性)。
  4. 返回新对象

上面的例子中,person1 和 person2是Person的不同实例。每个实例对象都有一个constructor属性指回Person,如下:

alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
//因为Person也继承Object
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

构造函数和普通函数

构造函数与普通函数唯一的区别就是调用方式的不同。对于同一个函数,如:

//use as a constructor
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
//call as a function
Person("Greg", 27, "Doctor); //adds to window
window.sayName(); //"Greg"
//call in the scope of another object
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

如果不显示设置this(通过对象方法或者apply()/call()),this永远指向全局Global对象(浏览器中的window)

问题

每初始化一个实例就会方法就会创建一次。逻辑类似于下面这样:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); //logical equivalent
}

所以

alert(person1.sayName == person2.sayName); //false

解决方法:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
//解决了创建重复函数的问题
function sayName(){
    alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

但是这样也会有不足,1、全局会多出一些方法 2、不够“封装”

原型模式

每一个函数都是通过一个由方法和属性组成的对象(prototype)创建的。这个对象是构造函数调用时的原型。使用原型的优势是,所有的对象实例都共享这个原型属性和方法。替代了把将信息写在构造函数中,可以将信息放在prototype里。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

prototype是如何工作的

根据上个例子

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

很简单

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
//在实例中找到这个属性
alert(person1.hasOwnProperty("name")); //false
//无论在实例中或者原型中找到这个属性
alert("name" in person2); //true
person1.name = "Greg";
alert(person1.name); //"Greg" - from instance
alert(person1.hasOwnProperty("name")); //true
alert("name" in person2); //true
alert(person2.name); //"Nicholas" - from prototype
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
//删除实例属性
delete person1.name;
alert(person1.name); //"Nicholas" - from the prototype
alert(person1.hasOwnProperty("name")); //false
alert("name" in person2); //true
//我感觉有歧义,不讨论了
function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}

下面的方法支持的浏览器:Internet Explorer 9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //”constructor,name,age,job,sayName”

另一种原型语法

简化型:

function Person(){
}
Person.prototype = {
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

这种写法和之前不同的地方是,原型的constructor方法不在是Person了

var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

当然你可以修改一下:

function Person(){
}
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

原型的动态特性

var friend= new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi(); //"hi" - works!

执行friend.sayHi()时会在friend实例中查找,然后再去原型中查找,由于原型和实例之间的联系是一个指针而非拷贝。

尽管属性和方法可以在任何时候添加到原型里,它们也会立刻反应在所有的实例上。但是你不能重写整个原型以期望达到类似的效果。

当构造函数被调用的时候,[[Prototype]] 指针被赋值,所以改变原型指向另外的对象就会切断构造函数和原型的纽带。再次强调:实例拥有一个指向原型的指针而不是构造函数:

function Person(){
}
var friend = new Person();
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName(); //error

放生了什么?
这里写图片描述

重写原型意味着新创建的实例指向这个原型,已经存在的实例还是指向以前的原型。

原生对象原型

原型模式同样应用于原生对象上,sort() 方法可以在Array.prototype() 找到,substring() 可以在String.prototype() 找到。
原生对象的原型也可以添加方法,如:

String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true

虽然可以,但是不推荐

原型的问题

各种实例在原型共享的属性如果是函数效果很好,如果是基础变量(primitive values)也正常,真正有问题的是如果属性包含引用类型(reference values)。

function Person(){
}
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

合并构造模式和原型模式

修改后

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor: Person,
    sayName : function () {
        alert(this.name);
    }
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

动态原型模式

把构造函数和原型分开有点奇怪

function Person(name, age, job){
    //properties
    this.name = name;
    this.age = age;
    this.job = job;
    //methods
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

寄生构造模式

function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
    alert(this.name);
};
    return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

这和工场模式的唯一区别就是使用了new操作符,

function SpecialArray(){
//create the array
var values = new Array();
//add the values
values.push.apply(values, arguments);
//assign the method
values.toPipedString = function(){
return this.join("|");
};
//return it
    return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

注意:返回对象和构造函数以及构造函数的原型没有任何关系。不能使用instanceof操作符获取对象类型。因为这个原因,当其它模式起作用时,是不应该使用这个模式的。

Durable Constructor Pattern

function Person(name, age, job){
    //create the object to return
    var o = new Object();
    //optional: define private variables/functions here
    //attach methods
    o.sayName = function(){
        alert(name);
    };
    //return the object
    return o;
}
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

这种模式,你无法通过返回的值来获取构造函数传入的数据,无论对返回对象做什么操作也不可以。

继承

面向对象的继承有两种:接口签名的继承(JS不支持),方法的继承(依靠原型链)。

原型链

ECMA-262 把prototype chaining 描述成js继承的主要的方法。引用类型之间利用原型的概念来继承方法和属性。构造函数、原型和实例的关系:构造函数都有一个指向本身的原型对象。实例拥有一个指向原型的指针。如果这个原型是另一个类型的实例呢?代表这个原型拥有一个指向其它原型的指针,以此类推。
实现原型链:

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true

这里定义了两个类型:SuperType 和 SubType,每个类型都只有唯一的属性和方法。主要的不同是 SubType 继承自 SuperType,用new 构造了SuperType的实例,并把它赋给 SubType.prototype,用一个新对象覆盖了SubType 的原prototype,话句话说,现在在SuperType里的属性和方法在SubType.prototype里也有。继承发生之后,赋给SubType.prototype一个新方法,给从SuperType继承来的方法的顶端添加了新方法,过程如下:
这里写图片描述

to be continued!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值