面向对象的程序设计
ECMA-262定义对象:无序属性的集合,其属性可以包含基本值,对象或者函数。
普通理解:对象是一组没有特定顺序的值。
对象的每个属性或方法都有一个名字,而每个名字都映射一个值。
每个对象都是基于一个引用类型创建的。
理解对象
属性类型
在定义只有内部的特征(attribute)时,描述了属性(property)的各种特征。
是为了实现JavaScript引擎用的,在JavaScript中不能直接访问他们。
为了表示特性是内部值,把它们放在了两对方括号中. 例如: [[Enumerable]]
ECMAScript中有两种属性:数据属性和访问器属性。
数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述行为的特性
-
[[Configurable]]
: 能否通过delete删除属性从而重新定义属性,能够修改属性的特性,或者能否把属性修改为访问属性。 默认值:false;(不可以重新定义或删除) -
[[Enmuerable]]
: 能够通过for-in循环返回属性。(是否可以被枚举).默认值:false;(不可以被枚举) -
[[Writable]]
: 能否修改属性的值。默认值:false,(不可以被修改) -
[[Value]]
:包含这个属性的数据值,读取属性的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值:undefiend.
var Person = {
name: 'Nicholas'
}
// 创建name属性。为它置顶的值是“Nicholas”。也就是[[Value]]被设置了"Nicholas",而对这个值的任何修改都将反映在这个位置。
Object.defineOProperty();
作用:修改属性默认特性
参数1:属性所在的对象
参数2:属性的名字
参数3:一个描述符对象
描述符(descriptor)对象的属性必须是: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"
constructor属性,是无法被枚举的. 正常的for-in循环是无法枚举. [eable = false];
Object.getOwnPropertyNames(); //枚举对象所有的属性:不管该内部属性能够被枚举.
//3个参数, 参数1:重新设置构造的对象 (给什么对象设置) 参数2:设置什么属性 参数3:options配置项 (要怎么去设置)
Object.defineProperty(Person.prototype,'constructor',{
enumerable: false, //是否是 能够 被枚举
value: Person //值 构造器的 引用
});
访问器属性
访问器属性不包含数据值,包含一堆geter() 和setter(); 函数 (这两个函数都不是必须的)
在读取访问器属性时,会调用getter(); 这个函数负责返回有效值,在写入访问器属性时,会调用setter()函数并传入新值,这个函数负责决定如何处理数据。
访问器属性的4个特性:
-
[[Configurable]]
: 能够通过delete删除属性从而定义新的属性,能否修改属性的特性,或者能否把属性修改为数据属性。默认值:false; (不可重新定义,和删除属性) -
[[Enmuerable]]
:能否通过for-in循环返回属性。默认值:false;(不可被枚举) -
[[Get]]
: 在读取属性时调用的函数。默认值为:undefeind -
[[Set]]
: 在写入属性时调用的函数。默认值为:undefiend
访问器属性不能直接定义,必须使用Object.defineProperty();来定义。
var book = {
_year: 2016,
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2016) {
this._year = newValue;
this.edition += newValue - 2016;
}
}
});
book.year = 2020;
alert(book.edition); // 5
// _year前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。
定义多个属性
Object.defineProperties();
定义多个属性。
参数1:对象要添加和修改其属性的对象
参数2:对象的属性与第一个对象中要添加或修改的属性一一对应.
var book = {};
Object.defineProperties(book, {
_year: {
value: 2016
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (newValue) {
this._year = newValue;
}
}
});
读取属性的特性
Object.getOwnPropertyDescriptor()
取得给定属性的描述符.
参数1:属性所在的对象
参数2:要读取其描述符的属性名称
返回值:对象。如果是访问器属性,这个对象含有:configurable,enumerable,get和set
如果是数据属性,这个对象含有:configureable,enumerbale,writeable,value
创建对象
工厂模式
解决:创建多个相似对象。(未解决:对象识别的问题-怎么知道一个对象的类型)
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");
构造函数模式
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");
- 没有显示的创建对象
- 直接将属性和方法赋给this对象
- 没有reutrn语句
创建实例会经过:
- 创建一个对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
对象的 constructor 属性最初是用来表示对象类型。
提到检测对象类型,还是使用instanceof操作符要更可靠。
将构造函数当作函数
构造函数与其它函数的唯一区别,在于调用它们的方式不同。
构造函数也是函数,不存在定义构造函数的语法。任何函数,只要通过new操作来调用,就可以作为构造函数。
任何函数,如果不通过new操作符来调用,就跟普通函数没什么不同。
构造函数的问题
每个方法都要在每个实例上重新创建
通过原型模式来解决
原型模式
每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
每个函数都有一个prototype(原型)属性,这个属性石一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
字面的理解:prototype就是通过调用构造函数而创建的那个对象实例的原型。
让所有对象实例共享它所有包含的属性和方法。不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型中。
理解原型对象
只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
默认情况下:所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
原型链查找:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对戏那个中找到这属性,则返回该属性的值。
可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值,如果在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
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();
person1.name = "Greg";
alert(person1.name); //"Greg" —— 来自实例的name
alert(person2.name); //"Nicholas" -- 来自原型中的name
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。添加这个属性只会组织访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。可以使用delete操作符可以完全删除实例属性。
hasOwnProperty() // 从Object继承而来的方法
检测一个属性是否存在于实例中,还是存在于原型。
判断一个对象属性 是属于 原型属性 还是属于 实例属性
在实例中,返回true
在原型上,返回false
function Person() {}
Person.prototype.name = 'Nicholas';
var p1 = new Person();
console.log(p1.hasOwnProperty('name')); // false
原型与in操作符
in 操作符,通过对象能够访问给定属性时返回true,无论是在实例中还是原型中。能够访问到,就返回true.
同时使用hasOwnProperty(); 和 in操作符 : 确定是属性存在实例中,还是原型中。
// 判断属于原型上的属性
function hasPrototypeProperty( obj, attr ) {
return !obj.hasOwnProperty(attr) && attr in obj;
}
Object.keys()
参数:接收一个对象作为参数,返回一个包含所有可枚举的属性的字符串数组。
function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 20;
var p1 = new Person();
var keys = Object.keys(Person.prototype);
console.log(keys); // ["name", "age"]
更简单的原型方法
function Person() {}
Person.prototype = {}
将Person.prototype设置为等于一个对象字面量形式创建的新对象。
会造成constructro属性不再指向Person。
每创建一个函数,就会同时创建它的peorotype,这个对象也会自动获得constructro属性。直接赋值为一个字面量形式,本质上是完全重写了默认的prottoype对象,因此constructor属性也就变成新的对象的constructor属性(指向Object构造函数) 不再指向Person函数。
通过instanceof操作符能够返回正确的结果,但是通过constructor已经无法确定对象的类型了。
可以手动设置回constructor属性为当前的类
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Nicholas'
}
这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true
constructor是不可被枚举,可以通过Object.definePropert();修改特性,重置构造函数。
Object.defineProperty(Person.protype, 'constructor', function () {
enmuerable: false,
value: Person
});
原型动态
由于在原型中查找值的过程是一次搜索,对原型对象所做的任何修改都能够立即从实例上反应出来,即使是先创建了实例后修改原型也是一样。
实例中的指针仅指向原型,而不指向构造函数。
可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写了这个原型对象,那么就不一样。
调用构造函数时会为实例添加一个指向最初原型的[[Prototype]] 或者__proto__ .而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
function Person () {}
var firend = new Person();
Person.prototype = {
constructor: Person,
name: 'Nicholas',
sayName: function () {
console.log(this.name);
}
}
firend.sayName(); // error // friend 指向的原型中不包含以该名字命名的属性
原生对象的原型
原型模式的体现在创建自定义类型方面,所有的原生的引用类型,都是采用这种模式创建的。
所有元素引用类型(Object,Array,String...)都在其构造函数上定义了方法
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新的方法。
console.log( typeof Array.prototype.slice ); // function
不建议这样使用,会导致命名冲突,也可能意外的重写原生的方法。
原型对象的问题
原型模式省略了为构造函数传递参数初始化的,造成所有实例默认情况下都将取得相同的属性值。
原型模式最大的问题是由其共享的本性所导致。
对于包含引用类型的原型对象,如果修改其值,那么另个实例也会修改。
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
friends: ['Shelby', 'Court']
}
var p1 = new Person();
var p2 = new Person();
p1.friends.push('Van');
console.log(p1.friends); // ["Shelby", "Court", "Van"]
console.log(p1.friends); // ["Shelby", "Court", "Van"]
实例一般都要有属性自己的全部属性。这个问题造成很少会单独使用原型模式。
组合使用构造函数模式和原型模式
构造函数模式:用于定义实例的属性
原型模式:用于定义方法和共享的属性。
结果:每个实例都会有自己的一份实例属性的副本,但同时又共享这对方法的引用,最大限度的节省内存,同时还支持向构造函数传递参数。
是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。是用来定义引用类型的一种默认模式。
动态原型模式
把信息都封装到函数中,这样体现了封装的概念。
通过在构造函数中初始化原型(仅在必要情况下),同时保持了同时使用构造函数和原型的优点。
可以通过检查默认应该存在的方法是否有效,来决定是否需要初始化运原型。
//动态原型模式:(让你的代码 都封装到一起)
function Person( name,age,firends ) {
this.name = name;
this.age = age;
this.firends = firends;
//动态原型方法
if ( typeof this.sayName !== 'function' ) {
Person.prototype.sayName = function () {
console.log(this.name);
}
}
}
sayName()方法不存在的情况下,才会将它添加到原型中if语句中的代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改。这边的原型所做的修改,能够理解在所有实例中得到反映。
if语句的检查可以是初始化之后应该存在的任何属性或方法---不必用一大堆if语句检查每个属性和每个方法。只要检查其中一个即可。
采这种模式创建的对象,还可以使用instanceof操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,就会切断现有实例与新原型之间的联系。
寄生构函数模式
创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
在工厂模式,组合使用构造函数模式和原型模式,都不能使用的情况下,使用。
这个模式与工厂模式是类似的,构造函数在不返回值的情况下,默认会返回新对象的实例。而在构造函数末尾添加一个return语句,可以重写调用构造函数时返回出来的值。
function Person ( name ) {
var o = new Object();
o.name = name;
o.sayName = function () {
console.log(this.name);
}
return o;
}
var friend = new Person('Van');
friend.sayName();
寄生构造函数模式,首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系。
构造函数返回的对象与在构造函数外部创建的对象没有什么不同,为此,不恩呢该依赖 instanceof操作符来确定对象类型。
一般来说,不可通过prototype修改原生对象的方法。可以使用寄生构造函数模式,达到特殊需求。
稳妥构造函数模式
稳妥模式就是没有公共属性,而且其他方法也不引用this对象,稳妥模式最适合在安全的环境中使用。如果程序对于安全性要求很高,那么非常适合这种模式。
也不能使用new关键字。
//稳妥构造函数式 durable object (稳妥对象)
//1,没有公共的属性
//2,不能使用this对象
function Person ( name,age ) {
//创建一个要返回的对象。 利用工厂模式思维。
var obj = new Object();
//可以定义一下是有的变量和函数 private
var name = name || 'zf';
// var sex = '女';
// var sayName = function () {
// }
//添加一个对外的方法
obj.sayName = function () {
console.log(name);
}
return obj;
}
var p1 = Person('xixi',20);
p1.sayName();
继承
许多OO语言横纵都支持两种继承方式:接口继承 和实现继承。
接口继承:只继承方法签名
实现继承:继承实际的方法。
由于函数中没有签名,在ECMAScript中无法实现接口继承,ECAMScript 只支持实现继承,而其实现继承主要原型链来实现。
原型链
基本思想:让一个引用类型继承另一个引用类型的属性和方法。
让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含指向另一个构造函数的指针。
实现的本质是重写原型对象,代之以一个新类型的实例
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.getSuperValue() ); // true
调用 instance.getSuperValue()
会经历三个步骤:
1:搜索实例
2:搜索SubType.prototype
3:搜索SuperTpe.prototype
别忘记默认的原型
所有函数的原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符,只要用
这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true 。
方式1:使用instanceof操作符。 测试实例与原型链中出现的构造函数,就会返回true。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
方式2:使用isPrototypeOf(); 是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。就会返回true。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。
给原型添加方法的代码一定要放在替换原型的语句之后。
即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这
样做就会重写原型链,
即在通过原型链实现继承时,不能使用对象字面量创建原型方法。这样会重写原型链。导致替换原型无效。
原型链的问题
原型链可以实现继承,但是存在一些问题:包含引用类型的原型。
通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性了。
在构造函数中,而不是原型对象中定义属性的原因:包含引用类型的原型属性会被实例所共享。
第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。
原因:没有办法在不影响所有对象的实例情况下,给超类型的构造函数传递参数。
实践中很少单独使用原型链。
借用构造函数
在子类型构造函数的内部调用超类型构造函数
函数只是在特定环境中执行代码的对象,因此通过使用apply()和call()方法可以在新创建的对象上执行构造函数
传递参数
在类型构造函数中向超类型构造函数传递参数
function SuperType ( name ) {
this.name = name;
}
function SubType ( name ) {
// 继承了SuperType,同时传递参数
SuperType.call(this,name)
this.age = 21;
}
var instance = new SubType('cyan');
console.log( instance.name ); // cyan
console.log( instance.age ); // 21
借用构造函数的问题
无法避免构造函数模式存在的问题--方法都定义构造函数中定义。
超类型的原型中定义的方法,对子类型而言也是不可见的。
借用构造函数很少单独使用
组合继承
将原型链和借用构造函数的技术组合到一块,发挥二者之长的一种继承模式。
思路:使用原型链的实现对象原型属性和方法的继承,通过借用构造函数实现的对象属性的继承。
结果:即通过在原型上定义方法实现了函数复用,有能够保证每个实例都有它自己的属性。
instanceof 和isPrototypeOf(); 能够用于识别基于组合创建的对象。 (JavaScript中最常用的继承模式)
缺点:会执行构造函数二次。
原型式继承
借助原型可以基于已有的对象创建新对象,同时还不比因此创建自定义类型。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在 object(); 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上:boject()对传入其中的对象执行了一次浅复制
//2件事: 继承了1次父类的模板,继承了一次父类的原型对象
function Person ( name,age ) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person,
sayHello: function () {
console.log('hello world!');
}
}
function Boy ( name,age,sex ) {
//call 绑定父类的模板函数 实现 借用构造函数继承 只复制了父类的模板
// Person.call(this,name,age);
Boy.superClass.constructor.call(this,name,age);
this.sex = sex;
}
//原型继承的方式: 即继承了父类的模板,又继承了父类的原型对象。
// Boy.prototype = new Person();
//只继承 父类的原型对象
extend(Boy,Person); // 目的 只继承 父类的原型对象 , 需要那两个类产生关联关系.
//给子类加了一个原型对象的方法。
Boy.prototype.sayHello = function () {
console.log('hi,js');
}
var b = new Boy('zf',20,'男');
console.log( b.name );
console.log( b.sex );
b.sayHello();
Boy.superClass.sayHello.call(b);
//extend方法
//sub子类, sup 父类
function extend ( sub,sup ) {
//目的, 实现只继承 父类的原型对象。 从原型对象入手
//1,创建一个空函数, 目的:空函数进行中转
var F = new Function(); // 用一个空函数进行中转。
// 把父类的模板屏蔽掉, 父类的原型取到。
F.prototype = sup.prototype; // 2实现空函数的原型对象 和 超类的原型对象转换
sub.prototype = new F(); // 3原型继承
//做善后处理。 还原构造器 ,
sub.prototype.constructor = sub; //4 ,还原子类的构造器
//保存一下父类的原型对象 // 因为 ①方便解耦, 减低耦合性 ② 可以方便获得父类的原型对象
sub.superClass = sup.prototype; //5 ,保存父类的原型对象。 //自定义一个子类的静态属性 , 接受父类的原型对象。
//判断父类的原型对象的构造器, (防止简单原型中给更改为 Object)
if ( sup.prototype.constructor == Object.prototype.constructor ) {
sup.prototype.constructor = sup; //还原父类原型对象的构造器
}
}
Object.create()
参数1:用作新对象原型的对象。
参数2(可选)一个为新对象额外属性的对象. 配置参数(每个属性都是通过高自己的描述符定义)
传入一个参数的情况下:Object.create(); 与 object() 方法的行为相同.
注意:定义任何属性都会覆盖原型对象上同名属性。
包含引用类型值的属性始终都会共享相应的值
var person = {
name: 'cyan',
firends: ['tan', 'kihaki', 'van']
}
var anotherPerson = Object.create(person, {
name: {
value: 'red'
}
});
console.log( anotherPerson.name ); // red
寄生式继承
创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother ( original ) {
var cloen = Object.create(original); // 调用函数创建一个新对象
clone.sayHi = function () { // 以某种方式来增强这个对象
console.log('hi');
}
return clone; // 返回这个对象
}
寄生组合式继承
解决,构造函数调用二次的问题。
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
基本思路:不必为了指定子类型的原型而调用超类型构造函数。需要的是超类型的原型的一个副本。
本质:使用寄生式继承来继承超类型的原型,然后再将指定给子类型的原型。
没有使用new关键字
function inheritProtoype ( subType, superType ) {
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
// 1: 创建超类型的原型的一个副本。
// 2:为创建的副本添加添加constructor属性,从而弥补因为重写原型而市区去的constructor属性
// 3: 将新创建的对象,赋值给子类型的原型。
函数表达式
通过name 可以访问函数名(非标准属性)
关于函数声明,重要特征就是函数声明提升:代码执行之前会先读取函数声明。
匿名函数(也叫拉姆达函数): function 关键字后面没有标识符
匿名函数的name属性时空字符串
递归
递归函数: 一个函数通过名字调用自身的情况
function factorial (num) {
if ( num <= 1 ) { // 递归出口
return 1;
} else {
return num * arguments.callee(num-1); // 递归点
}
}
命名函数表达式:
var factorial = (function f ( num ) {
if ( num <= 1 ) {
return 1;
} else {
return num * f(num-1);
}
});
创建了一个名为f() 的命名函数表达式,然后将它赋值给变量factorial。即使把函数赋值给另一个变量,函数的名字f仍然是有效的。
闭包
闭包:指有权访问另一个函数中作用域中的变量的函数。
表现:在一个函数内部创建另一个函数
当某个函数被调用的时候,会创建一个执行环境(execution context)及相应的作用域链。然后使用arguments和其它命名参数的值来初始化函数的活动对象(activation object)。
在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位直至作用域链的终点的全局执行环境。
后台的每个执行环境都有一个表示变量的对象---变量对象
作用域链中至少包含二个变量对象:本地活动对象和全局变量对象。
作用域链本质:一个指向变量对象的指针列表,引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内容中近保存全局作用域(全局执行环境的比变量对象)。
但是,闭包可以延长变量的生存周期
function createComparisonFunction( propertyName ) {
return function ( object1, object2 ) {
var val1 = object1[propertyName];
var val2 = object1[propertyName];
if ( val1 < val2 ) {
return -1;
} else if ( val1 > val2 ) {
return 1;
} else {
return 0;
}
}
}
var compare = createComparisonFunction('name');
var reslut = compare({name: 'cyan'}, {name: 'tan'});
// 告知垃圾回收机制将其清除,
// 随着匿名函数的作用域链被销毁,其它作用域(除了全局作用域)也都可以安全的销毁。
compare = null; // 接触对匿名函数的引用(以便释放内存)
createComparisonFunction()函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
当createComparisonFunction()函数执行返回后,其执行环境的作用域链会被销毁,但它的活动对象仍会留在内存中,直至匿名函数被销毁。createComparisonFunction()的活动对象才会被销毁。
闭包的问题:
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用过多的内存。过度使用闭包可能会导致内存占用过多,V8引擎优化后,JavaScript引擎会尝试回收被闭包占用的内存。
闭包与变量
闭包只能取得包含函数中任何变量的最后一值。(循环嵌套函数的i问题)
闭包所保存的是整个变量对象,而不是某个特殊的变量。
function createFunctions () {
var reslut = [];
for ( var i=0; i<10; i++ ) {
reslut[i] = function (num) {
return function () {
return num;
}
}(i);
}
return reslut;
}
定义一个匿名函数,并将立即执行该匿名函数的结果赋给数组。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,有创建并返回了一个访问num的闭包。这样,reslut数组中的每个函数都有自己num变量的一个副本,因此可以返回各自不同的数值。
关于this对象
this对象是运行时就函数执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。
匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点不明显。(在通过call()或apply()来改变函数执行环境的情况下,this就会指向其它对象。)
var name = 'window';
var object = {
name: 'object',
getNameFunc: function () {
return function () {
return this.name;
}
}
}
console.log( object.getNameFunc()() );
先创建了一个全局变量name,有创建一个包含name属性的对象。这个对象包含一个方法--getNameFunc(); 它返回一个匿名函数,而匿名函数又返回this.name。由于getNameFunc();返回一个函数,因此调用object.getNameFunc()(); 就立即调用它返回的函数,结果返回一个字符串。 结果的name变量的值是全局的。为什么匿名函数没有取得其包含作用域(或外部作用域)的this对象呢?
每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这个两个变量。
可以通过保存this引用来访问
var name = 'window';
var object = {
name: 'object',
getNameFunc: function () {
var self = this;
return function () {
return self.name;
}
}
}
console.log( object.getNameFunc()() );
this 和arguments存在同样的问题,如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
内存泄漏
如果闭包的作用域链保存着一个HTML元素,就意味着该元素将无法被销毁
闭包会引用包含函数的整个活动对象。
包含函数的活动中也仍然会保存一个引用。因此,有必要把element变量设置null。这样能够接触对DOM对戏那个的引用。顺利地减少其引用数据,确保正常回收其占用的内存。
模仿块级作用域
JavaScript中没有块级作用域的概念
function outputNumbers ( count ) {
for ( var i=0; i<count; i++ ) {
console.log(i);
}
console.log(i); // 计数
}
变量i是定义在outputNumbers()的活动对象中,因此从它有定义开始,就可以在函数内部随处访问它。
匿名函数用来模仿块级作用域并避免这个问题。
块级作用域:称为私有作用域
(function () {})();
将函数声明包含在一堆圆括号中,表示它实际上是一个函数表达式,而紧随其后的另一对圆括号会立即调用这个函数。
JavaScript将function 关键字作一个函数声明的开始,而函数后声明不能跟圆括号。然后函数表达式的后面可以跟圆括号。
将函数声明转换成表达式使用()
需求:只要临时需要一些变量,就可以使用私有化作用域
function outputNumbers ( count ) {
(function () {
for ( var i=0; i<count; i++ ) {
console.log(i);
}
})();
console.log(i); // 报错
}
// 变量i 只能在循环中使用,使用后即被销毁,而在私有化作用域中能够访问变量count,因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。
在匿名函数中的定义的任何变量,都会在执行结束时被销毁。
结果:减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
特权方法:有权访问私有变量和私有方法的公有方法。
作用:封装性,隐藏那些不应该被被直接修改的属性,方法。
缺点:必须使用构造函数模式来达到这个目的。
构造函数本身是有缺点:对每个实例都是创建同样一组新方法。
构造函数中定义特权方法:
function MyObject () {
// 私有变量和私有函数
var privateVariable = 10;
function prvateFunction () {
return false;
}
// 特权方法
this.publicMethod = function () {
privateVariable++;
return prvateFunction();
}
}
静态私有变量
目的:创建静态变量会因为使用原型而增进代码的复用,但没有实例对象都没有自己的私有变量。
使用闭包和私有变量缺点:多查找作用域中的一个层次,就会在一定程度上影响查找速度。
模块模式
目的:为单例创建私有变量和特权方法。
本质:对象字面量定义的是单例的公共接口
单例:只有一个实例的对象。
惯例:JavaScript是以对象字面量的方式来创建单例对象。
作用:对单例进行某些初始化,同时又需要维护其私有化变量。
//通过 一个私有变量来控制是否 实例化对象, 初始化一个 init。
var Ext = {};
Ext.Base = (function () {
//私有变量 控制返回的单体对象
var uniqInstance;
//需要一个构造器 init 初始化单体对象的方法
function Init () {
//私有成员
var a1 = 10;
var a2 = true;
var fun1 = function () {
console.log( a1 );
}
return {
attr1: a1,
attr2: a2,
fun1: fun1
}
}
return {
getInstance: function () {
if ( !uniqInstance ) { //不存在 ,创建单体实例
uniqInstance = new Init();
}
return uniqInstance;
}
}
})()
var init = Ext.Base.getInstance();
init.fun1(); //10
使用需求:创建一个对象并以某些数据对齐初始化,同时还要公开一些能够访问这些私有数据方法。
每个单例都是Object的实例,因为通过一个对象字面量表示单例通常都是 全局对象存在,不会将它传递给一个函数。
增强的模块模式
在返回对象之前假如对其增强的代码。
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//创建 application 的一个局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
}
app.registerComponent = function(component){
if (typeof component == "object"){
components.push(component);
}
}
//返回这个副本
return app;
}();
BOM
window对象
BOM的核心对象是window,表示浏览器的一个实例。
在浏览器中,window对象有双重角色:
1:JavaScript访问浏览器窗口的一个接口
2: ECMAScript规定的Global对象。
在网页中定义的任何一个对象,变量,函数,都已window作为其Global对象。因此有权访问parseInt()等方法。
窗口与框架
使用框架时,每个框架都有自己的window对象以及所有原生构造函数及其它函数的副本。每个框架都保存在frames集合中,可以通过位置或通过名称来访问。
top对象始终指向最外围的框架,也就是整个浏览器窗口。
parent 对象表示包含当前框架的框架,而 self 对象则回指 window 。
全局作用域
定义全局变量与在window对象上直接定义属性差别:
全局变量不能通过delete操作删除,直接定义window对象上的定义属性可以删除。
var age = 22;
window.color = "red";
//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 false
delete window.age;
//在 IE < 9 时抛出错误,在其他所有浏览器中都返回 true
delete window.color; // returns true
console.log(window.age); //22 29
console.log(window.color); // undefined
var 语句添加的window属性[Configurable]的特性,这个特性的值被设置为false。所以定义的不可delete操作符删除。
尝试访问未声明的变量会抛出错误,但是通过window对象,可以知道某个可能未声明的变量是否存在。
var newValue = oldValue; // 报错,未定义
var newValue = window.oldValue; // 属性查询,不会抛出错误
location对象
提供了与当前窗口中加载的文档有关的信息,提供一些导航功能。
location对象很特别,即是window对象的属性,也是document对象的属性,window.location 和 document.location引用的是同一个对象。
作用:保存着当前文档的信息,还表现将URL解析为独立的片段。
属性 | 例子 | 说明 |
---|---|---|
hash | "#contents" | 返回URL中的hash(#号后跟零或多个字符),如果URL中不包含散列,则返回空字符串 |
host | "www.aa.com:80" | 返回服务器名称和端口号(如果有) |
hostname | "www.aa.com" | 返回不带端口号的服务器名称 |
href | "http:/www.aa.com" | 返回当前加载页面的完整URL。而location对象的toString()方法也返回这个值 |
pathname | "/WileyCDA/" | 返回URL中的目录和(或)文件名 |
port | "8080" | 返回URL中指定的端口号。如果URL中不包含端口号,则这个属性返回空字符串 |
protocol | "http:" | 返回页面使用的协议。通常是http:或https: |
search | "?q=javascript" | 返回URL的查询字符串。这个字符串以问号开头 |
位置操作
location对象改变浏览器位置
location.assign('http://segmentfault.com'); // 打开新的URL并在浏览器的历史记录中生成一条记录。
如果将location.href 或window.location设置为一个URL,会以该值调用assign()方法。
window.location = "http://www.segmentfault.com";
location.href = "http://www.segmentfault.com";
最常用的方式:location.href = "http://www.segmentfault.com";
禁止用户使用‘后退’按钮 使用location.replace();
参数:需要导航的URL
location.relaod(); 作用:重新加载当前显示的页面
location.reload(); //重新加载(有可能从浏览器缓存中加载)
location.reload(true); //重新加载(从服务器重新加载)
location.reload();调用之后的代码可能会也可能不会执行,取决于网络延迟或系统资源等因素。如果使用location.relaod();最好放在代码最后一行。
navigator对象
作用:识别客户端浏览器
navigator.userAgent // 浏览器的用户代理字符串
检测插件
非IE下使用:navigator.plugins数组
function hasPlugin(name){
name = name.toLowerCase();
for (var i=0; i < navigator.plugins.length; i++){
if (navigator. plugins [i].name.toLowerCase().indexOf(name) > -1){
return true;
}
}
return false;
注册处理程序
registerContentHandler() 和 registerProtocolHandler()
作用:让一个站点指明它可以处理特定类型的信息。(使用范围:RSS 阅读器和在线电子邮件程序)
registerContentHandler();
参数1:要处理的MIME类型
参数2:可以处理该MIME类型的页面URL
参数3:应用程序的名称
// 站点注册为处理 RSS 源的处理程序
navigator.registerContentHandler("application/rss+xml","http://www.somereader.com?feed=%s", "Some Reader");
// 参数1:RSS源 的MIME类型参数 2:应该接收 RSS源 URL的 URL,其中的%s 表示RSS 源 URL,由浏览器自动插入。
// 作用:当下一次请求 RSS 源时,浏览器就会打开指定的 URL,而相应的Web 应用程序将以适当方式来处理该请求。
screen对象
表明客户端能力:浏览器窗口外部的显示器信息,如:像素宽度和高度。
window.screen.height // 屏幕的像素高度
window.screen.windth // 屏幕的像素宽度
window.resizeTo(); // 调整浏览器窗口大小
history对象
history 对象:保存用户上网的历史记录。充窗口被打开的那一刻算起。
history是window对象的属性,因此每个浏览器串窗口,每个标签页乃至每个框架,都有自己的history对象与特定的window对象关联。
history.go(); 在用户的历史记录中任意跳转。
//后退一页
history.go(-1);
//前进一页
history.go(1);
//前进两页
history.go(2);
// go()参数是字符串 // 可能后退,也可能前进,具体要看哪个位置最近
//跳转到最近的 wrox.com 页面
history.go("segmentfault.com");
history.length; 保存着历史记录的数量
包括所有历史记录,即所有向后向前的记录。对于加载到窗口,标签页或框架中的第一个页面而言。
高级技巧
高级函数
安全的类型检测
JavaScript 中内置的类型检测机制并非完全可靠
typeof操作符,由于它有一些无法预知的行为,导致检测数据类型时得到不靠谱的结果。(Safari直至第四版,对正则表达式 typeof 检测 会返回 'function')
instanceof操作符,存在多个全局作用域(像一个页面中包含多个frame)的情况下。
var isArray = valeu instaceof Array;
// 返回true 条件:value 必须是数组, 必须与Array构造函数在同个全局作用域中。(Array 是 window的属性)。
在任何值上调用Object原生的toString();方法。
返回 [object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性中指定了这个字符串中的构造函数名.
应用:
检测原生JSON对象。
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) === '[object JSON]';
作用域安全的构造函数
作用:自定义对象和构造函数的定义和用法。
构造函数就是一个使用new操作符调用的函数。当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例。
问题:当没有使用new操作符来调用该构造函数的情况下。this对象是在运行时绑定的,所以直接调用 类名 ,this会映射到全局对象window上,导致错误对象属性的以外增加。
function Person ( name, age ) {
this.name = name;
this.age = age;
}
var p1 = Person('cyan', 22);
console.log(window.name);
console.log(window.age);
// Person实例的属性被加到window对象上,因为构造函数时作为普通函数调用,忽略了new操作符。由this对象的晚绑定造成的,在这里this被解析成了window对象。
// 在类中添加判断:
function Person ( name, age ) {
if ( this instanceof Person ) {
this.name = name;
this.age = age;
} else {
return new Person(name, age);
}
}
var p1 = Person('cyan', 22);
console.log(window.name);
console.log(window.age);
// 添加一个检查,并确保this对象是Person实例。
// 要么使用new操作符,要么使用现有的Person实例环境中调用构造函数。
函数绑定
函数绑定要创建一个函数,可以在待定的this环境中指定参数调用另一个函数。
使用范围:回调函数与事件处理程序一起使用,(在将函数作为变量传递的同时保留代码执行环境)
// bind(); 函数
function bind ( fn, context ) {
return function () {
return fn.apply(context, arguments);
}
}
将某个函数以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定的函数的效果。
主要用于事件处理程序以及setTimeout() 和 setInterval();
被绑定函数与普通函数相比有更多开销,需要更多内存支持,同时也因为多重函数调用调用比较慢。最好只在必要时使用。
函数柯里化
函数柯里化( function currying ): 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并返回接受余下的参数且返回结果的新函数
作用:创建已经设置好的一个或多个参数的函数。
函数柯里化的基本方法:使用一个闭包返回一个函数。
函数柯里化和函数绑定区别:当函数被调用时,返回的函数还需要设置一些传入的参数。
函数柯里化的动态创建:调用另一个函数并为它传入要柯里化的函数和必要的参数。
用处:
作为函数绑定的一部分包含在其中,构造出更为复杂函数
function bind ( fn, context ) {
let args = Array.prototype.slice.call(arguments, 2);
return {
let innerArgs = Array.prototype.slice.call(arguments);
let fianlArgs = args.concat(innerArgs);
return fn.apply(context, fianlArgs);
}
}
缺点:
每个函数都会带来额外的开销.不能够滥用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。