P4 面向对象的程序设计
4.1 理解对象
// 创建实例
var person = new Object();
person.name = 'hqy';
person.age = 21;
person.sayName = function() {
alert(this.name);
}
// 对象字面量
var person = {
name: 'hqy',
age: 21,
sayName; function() {
alert(this.name);
}
}
数据属性
-
Configurable
:能否通过delete
删除属性,能否修改属性的特性,能否把属性修改为访问器属性,默认true
-
Enumerable
:能否通过for-in
循环返回属性,默认true
-
Writable
:能否修改属性的值,默认true
-
Value
:这个属性的数据值,默认undefined
使用ES5的 Object.defineProperty()
修改属性默认的特性
// 参数:属性所在对象,属性名,一个描述符对象
var person = {};
Object.defineProperty(person, 'name', {
writable: false, // 设置成只读
value: 'hqy'
})
alert(person.name) // hqy
person.name = 'sss'; // 严格模式下报错
alert(person.name) // hqy
// 将 configurable 设置成 false,delete 无效,严格模式报错
// 一旦把属性定义为不可配置的,就不能再把它变回可配置了,修改除 writable 之外的特性都会导致错误
访问器属性
-
Configurable
:能否通过delete
删除属性,能否修改属性的特性,能否把属性修改为访问器属性,默认true
-
Enumerable
:能否通过for-in
循环返回属性,默认true
-
Get
:读取属性时调用的函数,默认undefined
-
Set
:写入属性时调用的函数,默认undefined
访问器属性不能直接定义,必须使用 Object.defineProperty()
来定义
var book = {
_year: 2004, // 下划线常用来表示只能通过对象方法访问的属性
edition: 1
};
Object.defineProperty(book, 'year', {
get: function() {
return this._year;
},
set: function(val) {
if(val > 2004) {
this._year = val;
this.edition += val - 2004;
}
}
})
book.year = 2005;
alert(book.edition) // 2
读取属性的特性
使用 Object.getOwnPropertyDescriptor(对象,属性名)
获得给定属性的描述符
4.2 创建对象-工厂模式
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
}
return o;
}
缺点:没有解决对象识别的问题,即怎么知道一个对象的类型
4.2 创建对象-构造函数模式
// 构造函数始终都应该以一个大写字母开头
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person('hqy', 21);
var person2 = new Person('sss', 22);
与工厂模式相比:1. 没有显式创建对象, 2. 直接将属性和方法赋给this
对象,3. 没有return
要创建Person
的新实例,必须使用new
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this指向这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
最后,person1
和 person2
分别报错着 Person
的一个不同的实例,每个对象都有一个 constructor
属性,指向 Person
。
alert(person1.constructor == Person) // true
alert(person2.constructor == Person) // true
// 检测对象类型
alert(person1 instanceof Person) // true
alert(person1 instanceof Object) // true
将构造函数当作函数
// 当构造函数
var person = new Person('hqy', 21);
person.sayName() // hqy
// 当普通函数
Person('hqy', 21); // 添加到 window
window.sayName(); // 'hqy'
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'sss', 25);
o.sayName() // sss
缺点
每个方法都要在每个实例上重新创建一遍,即不同实例上的同名函数是不相等的
alert(person1.sayName == person.sayName) // false
解决办法
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
但这样如果对象需要定义很多方法,那么就要定义这么多的全局函数。。。
4.2 创建对象-原型模式 ★
理解原型对象
- 每个函数都有
prototype
,指向函数的原型对象 - 每个原型对象都有
constructor
(构造函数),指向prototype
属性所在函数的指针 -
Person.prototype.constructor
指向Person
- 每个对象可以用
__proto__
访问Prototype
- 这个连接存在与实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间
确定对象间是否存在这种关系
// 通过 isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)) // true
// Object.getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype) // true
alert(Object.getPrototypeOf(person1).name) // name 设置在原型上
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
- 搜索首先从实例开始。如果实例上找到了具有同名的属性。则返回该属性的值
- 上面没有找到,继续搜索指针指向的原型对象,找到返回,没找到返回
undefined
不能通过对象实例重写原型中的值,也就是说实例上的属性怎么操作都不会影响到原型
function Person() {}
Person.prototype.name = 'hqy';
var person1 = new Person();
var person2 = new Person();
person1.name = 'sss';
alert(person1.name) // 'sss'
alert(person2.name) // 'hqy'
// 只有在实例上删除该属性才能重新访问到原型中的属性
delete person1.name;
alert(person1.name) // 'hqy'
hasOwnProperty()
方法检测一个属性是存在于实例上还是原型上,只在给定属性存在于实例上才会返回 true
。
in
单独使用时会在通过对象能够访问给顶属性时返回 true
,无论在实例上还是原型中。
// 判断属性到底是存在于对象中还是实例上
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
枚举属性 Object.keys()
,Object.getOwnPropertyName()
function Person() {}
Person.prototype.name = 'hqy';
Person.prototype.age = 21;
Person.prototype.job = 'software engineer';
var key = Object.keys(Person.prototype);
alert(key) // name,age,job
var person = new Person();
person.name = 'bob';
var key = Object.keys(person);
alert(key) // name
var key = Object.getOwnPropertyName(Person.prototype);
alert(key) // constructor,name,age,job
更简单的原型语法
function Person() {}
Person.prototype = {
constructor: Person
// 以字面量创建,constructor 不再指向 Person
name: 'hqy',
age: 21,
job: 'software engineer'
}
原型动态性
- 重写整个原型对象,会切断构造函数与最初原型之间的联系
- 实例中的指针仅指向原型,不指向构造函数
function Person() {}
var friend = new Person();
// 重写整个原型对象,会切断构造函数与最初原型之间的联系
Person.prototype = {
constructor: Person
// 以字面量创建,constructor 不再指向 Person
name: 'hqy',
age: 21,
job: 'software engineer',
sayName: function() {
alert(this.name);
}
}
friend.sayName(); // error
原生对象的原型
alert(typeof Array.prototype.sort) // function
String.prototype.getLength = function() {
return this.length;
}
缺点
function Person() {}
Person.prototype = {
constructor: Person
name: [1, 2], // 该属性会被实例共享
sayName: function() {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2,3
alert(person1.name === person2.name) // true
4.2 创建对象-构造函数+原型模式
function Person(name, age) {
this.name = name;
this.age = age;
this.friend = [1, 2];
}
Person.prototype = {
constructor: Person
sayName: function() {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.push(3);
alert(person1.sayName); // 1,2,3
alert(person2.sayName); // 1,2
alert(person1.name === person2.name) // false
4.2 创建对象-动态原型模式
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
function Person(name) {
// 属性
this.name = name;
//方法
// 初次调用构造函数时才会执行
if(typeof this.sayName !== 'function') {
Person.prototype.sayName = function() {
return this.name;
}
}
}
4.2 创建对象-寄生构造函数模式
寄生构造函数返回的对象与构造函数或者构造函数的原型属性之间没有关系,不能依赖 instanceof
确定对象类型。
// 创建一个具有额外方法的特殊数组
function SpecialArray() {
var list = new Array();
list.push.apply(list, arguments);
list.toPipedString = function() {
return this.join('|');
}
return list;
}
var color = new SpecialArray(1, 2, 3);
alert(color.toPipedString()) // 1|2|3
4.2 创建对象-稳妥构造函数模式
稳妥对象最适合在一些安全的环境中(这些环境禁止使用this
和new
),或者在防止数据被其他应用程序改动时使用。
- 新创建对象的实例方法不引用
this
- 不使用
new
操作符调用构造函数
function Person(name) {
var o = new Object();
// 可以在这里定义私有变量和函数
...
// 函数方法
o.sayName = function() {
alert(name);
}
return o;
}
var person = Person('hqy');
person.sayName() // hqy
除了调用 sayName()
,否则没其他方法访问其数据成员
4.3 继承-原型链
构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
假如我们让原型对象等于另一个类型的实例:
此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型也包含着一个指向另一个构造函数的指针。
假如另一个原型有事另一个类型的实例,那么上面的关系依然成立,如此层层递进,就构成了实例和原型的链条,这就是所谓的原型链。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 继承 SubType
SubType.prototype = new SuperType();
SubType.protptype.getSubValue = function() {
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue()) // true
instance
指向SubType
的原型,SubType
的原型又指向 SuperType
的原型。
getSuperValue()
方法仍然还在SuperType.protptype
中,但property
则位于SubType.prototype
中。这是因为property
是一个实例属性,而getSuperValue()
是个原型方法。
确定原型和实例的关系
// instacnceof
alert(instance instanceof Object) // true
alert(instance instanceof SuperType) // true
alert(instance instanceof SubType) // true
// isPropertyOf()
alert(Object.prototype.isPropertyOf(instance)) // true
alert(SuperType.prototype.isPropertyOf(instance)) // true
alert(SubType.prototype.isPropertyOf(instance)) // true
谨慎地定义方法
- 给原型添加方法一定能够要放在替换原型的语句之后
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样做会重写原型链,切断构造函数和原型的联系
原型链的缺点
包含引用类型值的原型属性会被所有实例共享。(4.2 创建对象-构造函数+原型模式 缺点已介绍)
4.3 继承-借用构造函数
function SuperType(name) {
this.name = name;
}
function SubType(name) {
// 继承了 SuperType,同时传递参数
SuperType.call(this, name);
// 实例属性
this.age = 21;
}
var instance = new SubType('hqy')
alert(instance.name); // hqy
alert(instance.age); // 21
缺点
仅仅是借用构造函数,方法都在构造函数中定义,因此函数复用就无从谈起了。
4.3 继承-组合继承(借用构造+原型链)
使用原型链实现对原型属性和方法的继承,而通过借用构造函数实现对实例属性的继承
function SuperType(name) {
this.name = name;
this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
return this.name;
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType; // ★
SubType.prototype.sayAge = function() {
return this.age;
}
var instance1 = new SubType('hqy', 21);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3,4
alert(instance.sayName()) // hqy
alert(instance.sayAge()) // 21
var instance2 = new SubType('sss', 25);
instance1.colors.push(4);
alert(instance1.colors) // 1,2,3
alert(instance.sayName()) // sss
alert(instance.sayAge()) // 2251
4.3 继承-原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在 object
函数内部,先创建了一个临时性的构造函数,然后将传入额对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
var person = {
name: 'hqy',
friends: [1, 2, 3, 4]
}
var a = object(person);
// 等价于 var a = Object.create(person);
a.friends.push(5);
var b = object(person);
a.friends.push(6);
alert(person.friends) // 1, 2, 3, 4, 5, 6
- 返回的这个新对象将
person
作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。 - 这意味着
person.friends
不仅属于person
所有,而且也会被a
和b
共享。 - 包含引用类型值的属性始终都会共享相应的值
4.3 继承-寄生式继承
该函数在内部以某种方式来增强对象
fucntion createAnother(origin) {
var clone = object(origin); // 创建一个新对象
clone.sayHi = function() { // 以某种方式来增强对象
alert('hi');
}
return clone; // 返回这个对象
}
var person = {
name: 'hqy',
friends: [1, 2, 3, 4]
}
var a = createAnother(person);
a.sayHi(); // hi
4.3 继承-寄生组合继承
组合继承是 JavaScript 最常用的继承模式,但它最大的问题在无论什么情况下,都会调用两次超类型构造函数
- 创建子类型原型时
- 子类型构造函数内部
组合继承的例子
function SuperType(name) {
this.name = name;
this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
return this.name;
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name); // 第二次调用 SuperType()
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType; // ★
SubType.prototype.sayAge = function() {
return this.age;
}
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型原型的一个副本而已
// 参数:子类型构造函数,超类型构造函数
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 创建超类型原型的一个副本
prototype.constructor = subType; // 增强对象,为副本添加constructor属性
subType.protptype = prototype; // 将副本复制给子类型的原型
}
function SuperType(name) {
this.name = name;
this.colors = [1, 2, 3];
}
SuperType.prototype.sayName = function() {
return this.name;
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
return this.age;
}
P5 函数表达式
- 函数声明提升,意思是在执行代码之前会项读取函数声明,但函数表达式只能当代码执行到它那一行后才会被解析。
- 创建一个函数并把它赋值给变量
functionName
,这种情况下创建的函数叫做匿名函数(拉姆达函数)
5.1 递归
arguments.callee
是一个指向正在执行的函数的指针
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
// 等价于 num * factorial(num -1),主要是为了防止函数名改变
}
}
5.2 闭包
闭包:是指有权访问另一个函数作用域的变量的函数。
创建闭包最常见的方式,就是在一个函数内部创建另一个函数
function createComparisonFunction(key) {
return function(obj1, obj2) {
var val1 = obj1[key];
var val2 = obj2[key];
if (val1 < val2) {
return -1;
} else if(val1 > val2) {
return 1;
} else {
return 0;
}
}
}
当一个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments
和其他命名参数的值来初始化函数的活动对象。但在作用域中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位。。。直至作为作用域链重点的全局执行环境。
作用域链本质上是一个指向变量对象的指针列表,它值引用当不实际包含变量对象。
在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()
函数内部定义的匿名函数的作用域中,实际上将会包含外部函数createComparisonFunction()
的活动对象。
createComparisonFunction()
函数执行完成后,其执行环境的作用域链会被销毁,但活动对象仍然留在内存中,知道匿名函数被销毁后,createComparisonFunction()
的活动对象才会被销毁。
var compareNames = createComparisonFunction('name') ;
var result = compareNames({ name: 'hqy' }, { name: 'sss' });
// 解除对匿名函数的引用,一遍释放内存
compareNames = null;
闭包和变量
function createFunction() {
var result = [];
for(var i = 0; i < 10; i++) {
result[i] = function() {
return i;
}
}
return result;
}
// 每个函数都返回 10
// 因为每个函数的作用域中都保存着createFunction的活动对象,所以它们引用的是同一个 i
// 返回一个立即执行的匿名函数解决问题
function createFunction() {
var result = [];
for(var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num;
}
}(i);
}
return result;
}
关于 this 对象
这里只是简单地介绍了下
var name = 'the window';
var object = {
name: 'the object',
getName: function() {
return function() {
return this.name;
}
}
}
// 内部函数在搜索 this, arguments 时,只会搜索到其活动对象为止
alert(object.getName()()) // 'the window' (非严格模式下)
要想获取 object 中的 name,这样做
var name = 'the window';
var object = {
name: 'the object',
getName: function() {
var that = this;
return function() {
return that.name;
}
}
}
alert(object.getName()()) // 'the object'
几种特殊情况
var name = 'the window';
var object = {
name: 'the object',
getName: function() {
return this.name;
}
}
object.getName() // 'the object'
(object.getName)() // 'the object',和上面的代码定义是一样的
(object.getName = object.getName)() // 'the window' (非严格模式下),因为先执行的时赋值语句
内存泄漏
function handle() {
var element = document.getElementById('someElement');
var id = element.id; // 消除循环引用
element.onclick = function() {
alert(id);
}
element = null; // 解除对DOM对象的引用,顺利减少去引用数
}
5.3 模块模式-单例
指只有一个实例的对象
var singleton = {
name: value,
method: function() {
// 这里是方法的代码
}
}
模块模式通过为单例添加私有变量和特权方法能够使其得到增强
var singleton = function() {
// 私有变量和私有函数
var privateVal = 10;
function privateFunc() {
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod: function() {
privateVal++;
console.log(privateVal);
return privateFunc();
}
}
}
var a = new singleton();
a.publicMethod(); // 11
a.publicMethod(); // 12
var b = new singleton();
b.publicMethod(); // 11
5.3 模块模式-增强的模块模式
var singleton = function() {
// 私有变量和私有函数
var privateVal = 10;
function privateFunc() {
return false;
}
// 创建对象
var obj = new CustomType();
//添加特权/公有方法和属性
obj.publicProperty = true;
obj.publicMethod = function() {
privateVal++;
console.log(privateVal);
return privateFunc();
}
// 返回这个对象
return obj;
}