Erik's Blog

《JavaScript 高级程序设计》笔记(2) —— 面向对象及函数表达式

(更新于 )JavaScript#ECMAScript

JavaScript 核心 II : 面向对象的程序设计、函数表达式

第 6 章 面向对象的程序设计

ECMAScript 中对象的定义 : 无序属性的集合,其属性可以包含基本值、对象或函数。

属性类型

数据属性(IE8+)【ie9+、fixfox 4+、Safari 5+、Opera 12+、Chrome】

特性名 作用 默认值
[[Configurable]] 可 delete 而重新定义属性、可修改属性的特性、可把属性修改为访问器属性 true
[[Enumerable]] for-in 循环返回属性 true
[[Writable]] 可修改属性值 true
[[Value]] 属性值 undefined

修改属性默认的特性 : Object.defineProperty(属性所在对象, 属性名, 描述符对象) 。描述符(descriptor)对象 可包含 configurable enumerable writable writable value IE8+

var person = {};
Object.difineProperty(person, "name", {
	writable: false,
	value: "llissery"
});

访问器属性

特性名 作用 默认值
[[Configurable]] 可 delete 而重新定义属性、可修改属性的特性、可把属性修改为访问器属性 true
[[Enumerable]] for-in 循环返回属性 true
[[Get]] 读取属性时调用的函数 undefined
[[Set]] 写入属性时调用的函数 undefined
var book = { _year: 2004, edition: 1 };

Object.defineProperty(book, "year", {
	get: function () { return this._year; },
	set: function (newValue) {
		if (newValue > 2004) {
			this._year = newValue;
			this.edition += newValue - 2004;
		}
	}
});

定义多个属性

var book = {};
Object.difineProperties(book, {
	_year: {
		writable: true,
		value: 2004
	},
	year: {
		get: function () {...},
		set: function () {...}
	}
});

读取属性的特性

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
使用  descriptor.value     descriptor.configurable    ...

访问器旧方法

_defineGetter_()
_defineSetter_()

创建对象

构造函数

对象字面量

工厂模式 被构造函数模式所取代

function createPerson (name) {
	var o = new Object();
	o.name = name;
	return o;
}

构造函数模式

原生或自定义构造函数,可以将他的实例标识为一种特定的类型(胜过工厂模式)。 创建一个新对象 –> 将构造函数的作用域赋给新对象 –> 执行构造函数中的代码 –> 返回新对象

新对象的 constructor 属性 指向 构造函数。 instanceof 检测对象类型更可靠

将函数的定义转移到构造函数外来解决美国方法都要在每个实例上重新创建的问题。

// 定义在 Global 对象中
function Person (name) { this.name = name; }
var p1 = new Person ("llissery");

原型模式(prototype)

将信息直接添加到原型中。

function Person () {};
Person.prototype.name = "llissery";

原型对象

# 构造函数 原型 实例 之间的关系

| Constructor |            |  Prototype  |           |    instance   |
|-------------|            |-------------|           |---------------|
|  prototype  | ---------> |             | <-------- | [[Prototype]] |
|             | <--------- | constructor |           | /__proto__    |
  • Constructor.prototype.isPrototypeOf(instance) 测试 instance 是否指向 Constructor.prototype
  • 在 ECMAScript 5 中 Object.getPrototype(instance) == Constructor.prototype, 取值的话采用 Object.getPrototype(instance).name IE9+
  • 实例中的属性会屏蔽原型对象中的同名属性。删除实例属性后恢复属性的连接 : delete instance.name
  • 检测一个属性是否存在于实例中 : instance.hasOwnPrototype(“name”);
  • 检测实例中或者原型中是否存在属性:“name” in instance
  • 取得对象上所有可以枚举的实例属性: Object.keys(Person.prototype) 或者 Object.keys(instance)
  • 返回所有实例属性 : key = Object.getPropertyNames(Person.prototype)
  • 重写整个原型对象时要重设 constructor 属性值 :
function Person () {};

// 1. 可枚举
Person.prototype = { constructor : Person, ...};

// 2. 只适用于 ECMAScript 5 的浏览器
Object.defineProperty(Person.prototype, "constructor", {
	enumerable: false,
	value: Person
});

原型对象的问题

  • 原型模式省略了构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的值。
  • 最大的问题是由其共享的本性所导致的。

组合使用构造函数换模式和原型模式 所有的默认原型都是 Object 实例

寄生(parasitic)构造函数模式 不能依赖 instanceof 操作符来确定对象类型

// 创建一个封装创建对象的代码的函数,返回新创建的对象。
function Person (name) {
	var o = new Object();
	o.name = name;
	o.sayName = function () {
		alert(this.name);
	}
	// 重写调用构造函数时的返回值
	return o;
}
var friend = new Person("llissery");

稳妥构造函数模式 不能依赖 instanceof 操作符来确定对象类型

  • 稳妥对象 : 没有公共属性,方法也不引用 this 的对象。
  • 不同点 : 创建对象的实例方法不引用 this;不是有 new 操作符调用构造函数。
function Person (name) {
	// 创建要返回的对象
	var o = new Object();

	// 这里定义私有变量

	// 添加方法
	o.sayName = function () {
		alert(name);
	}

	// 返回对象
	return o;
}

var friend = Person("llissery");

继承

由于 ECMAScript 中无方法签名,所以无法实现接口继承,只能实现实现继承。主要通过原型链来实现继承。

原型链

概念 : 让原型对象等于另一个类型的实例,层层递进,构成实例与原型的链条。 确定关系 : 使用 instanceof 或 isPrototypeOf() 方法。

function SuperType () {
	this.property = true;
}

SuperType.prototype.getSuperValue = function () {
	return this.property;
};

function SubType () {
	this.subproperty = false;
}

// 继承了 SuperType
SubType.prototype = new SuperType();
// 添加或重写方法 放在继承之后
SubType.prototype.getSubValue = function () {
	return this.subproperty;
};

var instance = new SubType();
console.log(instance.geetSuperValue()); // true

借用构造函数 (又叫伪造对象、经典继承) 很少单独使用

在子类型构造函数中调用超类型构造函数。可以传参。

function SuperType (name) {
	this.name = name;
}

function SubType () {
	// 继承了 SuperType, 同时传递参数
	SuperType.call(this, "llissery");
}

组合继承 (伪经典继承) : 结合使用原型链和借用构造函数

function SuperType (name) {
	this.name = name;
	this.color = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
	console.log(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 () { console.log(this.age); };

原型式的继承

// 对传入对象进行浅复制
function object (o) {
	// 创建一个临时性的构造函数
	function F () {};
	// 将传入的对象作为这个构造函数的原型
	F.prototype = o;
	// 返回这个临时类型的一个新实例
	return new F();
}

ES5 使用 Object.create() 规范化了原型式继承。IE9+。 Object.create(instance, { name: {value: “llissery”} })   

寄生式继承 : 创建一个仅用于封装继承过程的函数

function createAnother (original) {
	var clone = object(original);                        // 通过调用函数创建一个新对象
	clone.sayHi = function () { console.log("Hi"); };    // 以某种方式增强这个对象
	return clone;                                        // 返回这个对象
}

寄生组合式继承

function inheritPrototype(SubType, SuperType) {
	var prototype = Object(superType.prototype); // 创建超类原型的一个副本
	prototype.constructor = subType;             // 为副本添加 constructor 属性
	subType.prototype = prototype;               // 将副本赋值给子类型的原型
}

第 7 章 函数表达式

定义函数的两种方式 : 函数声明、函数表达式。 区别 : 函数声明提升 (在执行代码之前会先读取函数声明)。

匿名函数(拉姆达函数)

递归

arguments.callee 指向正在执行的函数,严格模式下访问会导致错误。

非严格模式下 : 使用 arguments.callee 代替函数名解决更名后递归错误的问题。 严格模式下 : 通过创建命名函数表达式来避免。

var factorial = (function f (num) {
   if (num <= 1) {
       return 1;
   } else {
       return num * f (num - 1);
   }
});

闭包

  • 什么是闭包 : 有权访问另一个函数作用域中的变量的函数。
  • 如何创建闭包 : 在一个函数内部创建另一个函数。

作用域链的细节 : 执行环境、作用域链、变量对象。

当函数被调用时,会创建一个执行环境及相应的作用域链并初始化函数的活动对象 (使用 arguments 和命名参数的值来初始化)。 作用域链本质上是一个指向变量对象的指针列表,它只引用单不实际包含变量对象。 在作用域链中,被调用函数的活动对象处于第一位,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,…直到全局执行环境。

闭包与变量

闭包只能取得包含函数中任何变量的最后一个值

function createFunction () {
	var result = new Array();
	for (var i=0, i<10, i++) {
		result[i] = function () {
			return i;  // 所有的 num 都为 10
		}
	}
}

function createFunction () {
	var result = new Array();
	for (var i=0, i<10, i++) {
		result[i] = function (num) {
			return num;
		}(i);  // 创建一个立即执行的匿名函数使每个 num 变量都有一个副本

	}
}

this

this 对象是运行时基于函数的执行环境绑定的: 在全局函数中 this = window : (object.getNameFunc = object.getNameFunc)(); 函数作为某个对象的方法调用时 this = 该对象 : object.getNameFunc(); call()、apply() 改变函数的执行环境 this = 其它对象

通常 匿名函数的 this = window ,每个函数在调用时都会自动获取两个特殊变量 this 和 arguments,内部函数搜索这两个变量时,只会搜索到其活动对象为止,不肯直接访问外部函数中的这两个变量。 如果想访问作用域中的arguments 或 this ,必须将对该对象的引用保存到另一个闭包能访问的变量中。

var object = {
	name : "My Object";
	getNameFunc : function () {
		var that = this;        // 将外部作用域中的 this 保存在一个闭包能够访问的变量里供闭包访问
		return function () {
			return that.name;
		}
	}
}

仿块级作用域

使用匿名函数可以用来模仿块级作用域 : (function () { … })();

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

私有变量 : 任何在函数中定义的变量都可以认为是私有的 特权方法 : 有权访问私有变量和私有函数的公有方法

模块模式

单例 : 只有一个实例的对象。 模块模式通过为单例添加私有变量和特权方法使其得到增强。

增强的模块模式

在返回对象之前加入对其增强的代码。