JavaScript面向对象系列:六、对象模式(二)

构造函数中的私有成员

模块模式在定义单个对象的私有属性上十分有效,但是对于那些同样需要私有属性的自定义类型,也可以在构造函数中使用类型的模式来创建每个实例的私有数据。

例如

function Person (name){
var age = 25;

this.name = name;

this.getAge = function(){
return age;
}

this.growOlder = function(){
age++;
}

}

var person = new Person("laowang");

console.log(person.name); //"laowang"
console.log(person.getAge()); //25

person.age = 100;
console.log(person.getAge()); //25

person.growOlder();
console.log(person.getAge()); //26

如果需要所有实例可以共享的私有数据,可以结合模块模式和构造函数。

例如

var Person = (function (name){
var age = 25;

this.name = name;

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

InnerPerson.prototype.getAge = function(){
return age;
}

InnerPerson.prototype.growOlder = function(){
age++;
}

return InnerPerson;
})();

var person1 = new Person("laowang");
var person2 = new Person("xiaowang");

console.log(person1.name); //"laowang"
console.log(person1.getAge()); //25

console.log(person2.name); //"xiaowang"
console.log(person2.getAge()); //25

person1.growOlder();
console.log(person1.getAge()); //26
console.log(person2.getAge()); //26

JavaScript面向对象系列:六、对象模式(一)

前言

js有很多创建对象的模式,完成工作的方式也不是只有一种。可以随时定义自己的类型和自己的泛用对象。可以使用继承或者混入等其他技术令对象间行为共享。也可以利用js高级技巧来组织对象结构被改变。

私有成员和特权成员

js对象对象的所有属性都是公有的,且没有显式的方法指定某个属性不能被外界某个对象访问。然而,有时候可能不希望数据公有。

模块模式

模块模式是一种拥有私有数据的单件对象的模式。基本做法就是使用立即调用函数表达式(IIFE)来返回一个对象。IIFE是一种被定义后立即调用并产生结果的函数表达式,该函数表达可以包括任意数量的本地变量,它们在函数外不可见。因为返回的对象被定义在函数内部,对象的方法可以访问这些数据。(IIFE定义的所有对象都可以访问通用的本地变量)以这种方式访问私有数据的方法被称为特权方法。

基本格式如下

var yourObject = (function(){
//私有数据

return {
//公有方法和属性
};
})();

IIFE是js中一种很流行的模式,部分原因就是模块模式中的应用。

模块模式允许使用普通变量作为非公有对象属性。通过创建必报函数作为对象方法来操作它们。闭包函数就是一个可以访问其作用域外部数据的普通函数。

例如

var person = (function (){
var age = 25;

return {
name:"laowang",
getAge:function(){
return age;
},
growOlder:function(){
age++;
}
};
})();

console.log(person.name); //"laowang"
console.log(person.getAge()); //25

person.age = 100;
console.log(person.getAge()); //25

person.growOlder();
console.log(person.getAge()); //26

模块模式还有一个变种叫暴露模块模式,它将所有的变量和方法都组织在IIFE的顶部,然后将它们设置到需要被返回的对象上。可以使用暴露模块模式改写上面的例子。

例如

var person = (function (){
var age = 25;

function getAge(){
return age;
}

function growOlder(){
age++;
}

return {
name:"laowang",
getAge:getAge,
growOlder:growOlder
};
})();

console.log(person.name); //"laowang"
console.log(person.getAge()); //25

person.age = 100;
console.log(person.getAge()); //25

person.growOlder();
console.log(person.getAge()); //26

JavaScript面向对象系列:二、函数

在js中,函数气死就是对象。使函数不同于其他对象的决定性特点是函数存在一个被称为 [[Call]] 的内部属性。内部属性无法通过代码访问而是定义了代码执行时的行为。es为js的对象定义了多种内部属性,这些内部属性都用双重中括号来标注。

[[Call]] 属性是函数独有的,表明该对象是可以被执行的。由于仅函数拥有该属性,es定义typeof操作符对任何具有 [[Call]] 属性的对象返回”function”。这在过去曾导致一些问题,因为某些浏览器曾经在正则表达式中包含 [[Call]] 属性,导致后者被错误鉴别为函数。现在,所有的浏览器行为都一致。typeof 不会再将正则表达式鉴别为函数了.

声明还是表达式

函数具有两种字面形式

第一种:函数声明

function add(num1,num2){
return num1 + num2;
}

第二种:函数表达式

var add = function(num1,num2){
return num1 + num2;
}

这两种看起来相似,实际上有一个非常重要的区别

函数名声形式的会被提升至上下文(context)顶部,而函数表达式不会

所以就会有下面的情况

var result = add(5,5);

function add(num1,num2){
return num1 + num2;
}

这种实际上是能正确运行的,这是因为函数声明被提升至上下文顶部,好像被写成

function add(num1,num2){
return num1 + num2;
}

var result = add(5,5);

但是下面这种就会报错

var result = add(5,5);

var add = function(num1,num2){
return num1 + num2;
}

因为函数表达式不会被提升,所以上面执行时找不到函数。
不过,只要你始终在使用函数前定义他们,你就可以随意使用者两种字面形式.

参数

js的函数另一个独特之处在于你可以给函数传递任意数量的参数却不造成错误。因为函数参数实际被保存在arguments的类数组对象中。

函数的命名参数只不过是为了方便使用,并不是真正的限制了该函数可接受参数的个数。

函数的命名参数都是函数的期望参数,函数的length属性只会显示出期望参数的个数。

例如

情况一

function reflect(value){
return value;
}

console.log(reflect("1")); // "1"
console.log(reflect("1"),25); //"1"
console.log(reflect.length); //1

情况二

function reflect(){
return arguments[0];
}

console.log(reflect("1")); // "1"
console.log(reflect("1"),25); //"1"
console.log(reflect.length); //0

重载

大多数面向对象语言支持函数重载,它能让一个函数具有多个 函数签名(函数签名由函数的名字,参数的个数以及其类型组成) ,因为js可以接受任意数量的参数且没有类型限制,所以js没有函数重载。

例如

function sayMessage(message){
console.log(message);
}

function sayMessage(){
console.log("Default message");
}

sayMessage("Hello"); //"Default message"

这里为什么是 “Default message”,可以用对象来帮助理解

var sayMessage = new Function("message","console.log(message);");
sayMessage = new Function("message","console.log(\"Default message\");");
sayMessage("Hello!"); //Default message

但是js可以模仿函数重载

例如

function sayMessage(message){
if(arguments.length === 0){
message = "Default message";
}
console.log(message);
}
sayMessage("Hello!"); //Hello!

总结

js函数的独特之处在于它们同时也是对象,也就是说它们可以被访问、复制、覆盖,就像其他对象一样。js中的函数和其他对象最大区别在于函数对象有一个特殊的内部属性 [[Call]],包含了该函数的执行指令。typeof 操作符会在对象内部查找这个内部属性,如果找到,就返回 “function”

函数字面形式有两种:声明和表达式。函数声明会被提升至上下文顶部,而函数表达式不会。但是函数表达式可以用于任何可以使用值的地方,例如赋值语句、函数参数或者另一个函数的返回值。

函数是对象,所以存在一个构造函数 Function。

JavaScript面向对象系列:一、类型

js有5中原始类型 Boolean number string null undefined,所有原始类型的值都有字面形式。字面形式是不被保存在变量中的值。

js和其他许多语言一样,原始类型的变量直接保存原始值(而不是一个指向对象的指针)。当你将原始值赋值给一个变量时,改值将被赋值到变量中。也是就说,如果你使一个变量等于另一个变量时,每一个变量都有它自己的一份数据拷贝.例如

var color1 = "red";
var color2 = color1;

每个含有原始变量的变量使用自己的存储空间,一个变量的改变不会影响到其他变量.

虽然字符串、数字和布尔值是原始类型,但是他们也拥有方法(null和undefined没有方法).

尽管原始类型拥有方法,但是他们不是对象,js使他们看上起像对象一样,以此来提供语言上的一致性体验。

引用类型是指js中的对象,同时也是在语言中能找到的最接近类的东西。引用值就是引用类型的实例,也是对象的同义词。对象是属性的无序列表。

当你将一个对象赋值给变量时,实际是赋值给这个变量一个指针。将一个变量赋值给另一个变量时,两个变量各获得了一份指针的拷贝,指向内存中的一个对象。

js语言有垃圾收集功能,因此当你使用引用类型时无需担心内存分配。但是最好在不使用对象时将其引用解除,让垃圾收集器对那块内存进行释放。解除引用的最佳手段是将对象变量设置为null。

js内建类型 Array Date Error Function Object RegExp

鉴别引用类型,对于函数,typeof 返回的是 funcion,但是其他引用类型的返回都是object.其他的引用类型可以使用instanceof来鉴别。

es5引入Array.isArray(item)来鉴别数组

原始封装类型共有三种,String,Number,Boolean。这些特殊引用类型的存在使得原始类型用起来和对象一样方便。

字符串对象的存在仅用于该语句并在随后被销毁。

例如:

var name = "aaaa";
name.last = "bbbb"
console.log(name.last); //undefined

下面是实际在js引擎中实际发生的事

var name = "aaaa";
var temp = new String(name);
temp.last = "bbbb";
temp = null;
var temp = new String(name);
console.log(temp); //undefined
temp = null

实际上是在一个立刻就被销毁的临时对象上而不是字符串上添加了新的属性。之后当你试图访问该属性时,另一个不同的临时对象被创建,而新属性并不存在。虽然原始封装类型会被自动创建,在这些值上进行instanceof检查对应类型的返回值却都是false。

例如

var name = "aaa";
var count = 10;
var found = false;

console.log(name instanceof String); //false
console.log(count instanceof Number); //false
console.log(found instanceof Boolean); //false

这是因为临时对象仅在值被读取时被创建。instanceof操作符并没有真的读取任何东西,也就是没有临时对象的创建,于是它告诉我们这些值并不属于原始封装类型。
你也可以手动创建原始封装类型,但是有某些副作用

var name = new String("aaa");
var count = new Number(10);
var found = new Boolean(false);

console.log(typeof name); //"object"
console.log(typeof count); //"object"
console.log(typeof found); //"object"

手动创建原始封装类型实际会创建出一个object,这意味着typeof无法手动鉴别出你实际保存的数据类型

一个对象在条件判断语句中总被认为是true,无论该对象的值是不是等于false。

var found = new Boolean(false);
if(found){
console.log("Found")
}

//结果执行了,"Found"

总结

javascript中虽然没有类,但是有类型。每个变量或数据都有一个对应的原始类型或者引用类型。5种原始类型(字符串、数字、布尔值、空类型、未定义)的值会被直接保存在变量对象中。除了空类型,都可以用typeof来鉴别。空类型必须直接跟null进行比较才能鉴别。

引用类型是js中最接近类的东西,而对象则是引用类型的实例。可以使用new操作符或者字面量的形式创建新对象。通常可以用点号访问属性和方法,也可以用中括号。函数在js中也是对象,可以使用typeof来鉴别他们。至于其他引用类型,你应该用instanceof和一个构造函数来鉴别。

为了让原始类型看上去更像引用类型,js提供了三种原始封装类型:String、Number、Boolean。js会在背后创建这些对象是的你能够像使用普通对象那样直接使用原始值,但是这些临时对象在使用它们的语句结束时就立刻被销毁。虽然你也可以自己创建原始封装类型的实例,但是它们太容易令人误解,所以最好别这么干。

JavaScript面向对象系列:三、理解对象

js中的对象是动态的,可以在代码执行的任意时刻发生改变。基于类的语言会根据类的定义锁定对象,js对象没有这种限制。js编程的一大重点就是管理对象。

当一个属性第一次被添加给对象时,js在对象上调用一个名为 [[Put]] 的内部方法。 [[Put]] 方法会在对象上创建一个新的节点来保存属性们就像第一次在哈希表上添加一个键一样。这个操作不仅指定了初始值,也定义了属性的一些特征。调用 [[Put]] 的结果是在对象上创建了一个自有属性。一个自有属性表明仅仅该指定对象实例拥有该属性。该属性被直接保存在实力内,对该属性的所有操作都必须通过该对象进项。

当一个已有的属性被赋予一个新值的时候,调用一个名为 [[Set]] 的方法。该方法将属性的当前方法替换为新值。

var person = {
name:"laowang" //调用 [[Put]] 方法
};
person.age = 25; //调用 [[Put]] 方法

person.name = "xiaowang"; //调用 [[Set]] 方法

属性探测

不要使用一下模式探测属性是否存在

if(person.age){
//todo
}

因为当person.age 为一个对象、非空字符串、非零数字、true时,会判断为真。当person为 null、undefined、0、false、NaN或空字符串会判断为假。

更可靠的属性探测方式是使用 in 操作符。

例如

console.log("name" in person); //true
console.log("sex" in person); //false

使用 in 操作符,在大多数情况下是属性探测的最好的办法。它还有一个额外的好处是不会评估属性的值。当此类评估会导致性能问题或者错误时,这一点很重要。

在某些情况下,希望检查一个属性是不是自有属性。

in 操作符会检查自有属性和原型属性,所以不能检查出正确结果。

另一个方法是 hasOwnProperty方法,这个方法只有在属性存在并且是自有属性时才返回true。

例如

var person = {
name:"laowang" //调用 [[Put]] 方法
};

console.log("name" in person); //true
console.log(person.hasOwnProperty("name")); //true

console.log("toString" in person); //true
console.log(person.hasOwnProperty("toString")); //false

##删除对象属性

使用 delete 操作符可以将属性从一个对象中删除。将对象的一个属性设置为null并不能将属性从对象中彻底删除,这种做法只会将原值替换为null。

使用 delete 操作符会调用对象内部名为 [[Delete]] 的内部方法。相当于从哈希表中移除了一个键值对。

属性枚举

对象内部属性 [[Enumerable]] 设置为true,则属性可以循环遍历。可以使用 for-in 操作符来遍历一个对象。es5中新增 Object.keys() 来遍历对象的键,返回一个键组成的数组。

这两种操作符循环遍历有区别, for-in 操作符会遍历自有属性和原型属性,Object.keys() 只会遍历自由属性。

并不是所有属性都是可枚举的,实际上,对象大部分原生方法的 [[Enumerable]] 设置为false,可以使用 propertyIsEnumerable() 方法来检查一个属性是否为可枚举,每个对象都有这个方法。

访问器属性

getset 属性时访问器属性,可以定义属性被读取或者写入时候的行为。这两个属性可以只设置其中一个,如果只设置 get 这个属性变为只读,如果只设置 set 这个属性变为只写,这两种情况在严格模式下会报错.

例如

var person = {
name:"laowang", //调用 [[Put]] 方法
set name(value){
console.log('set');
this.name = value;
},
get name(){
console.log('get');
return this.name;
}
};

person.name; //get
person.name = 'xiaowang'; //set

##通用特征

有两个属性是数据和访问器都具有的。一个是 [[Enumerable]] ,决定了你是否可以遍历该属性。另一个是
[[Configurable]] ,决定了该属性是否可配置.可以随时删除或改变一个可配置属性。改变属性特征,可以使用 Object.defineProperty() 方法。

例如

var person = {
name:"laowang",
};

Object.defineProperty(person,"name",{
enumerable:false
});

console.log("name" in person); //true
console.log(person.propertyIsEnumerable("name")); //false

var propertites = Object.keys(person);
console.log(propertites.length); //0

Object.defineProperty(person,"name",{
configurable:false
});

delete person.name;
console.log("name" in person); //true
console.log(person.name); //laowang

Object.defineProperty(person,"name",{
configurable:true
}); //报错

一个属性被设置为不可配置了,就不能再修改了。无法将一个不可配置属性变成可配置属性 。删除一个不可配置属性,非严格模式下会失败,严格模式下会报错。

数据属性特征

数据属性拥有两个访问器属性不具备的特征:

第一个是 [[Value]] ,包含属性的值,在对象上创建属性时该特征被自动赋值。所有的属性的值都保存在 [[Value]] 中,即使值是一个函数。

第二个是 [[Writable]] ,该特种是一个布尔值,只是该属性是否可以写入,所有的属性默认是可写的。

例如

var person = {};

Object.defineProperty(person,"name",{
value:"laowang"
});

console.log("name" in person); //true
console.log(person.propertyIsEnumerable("name")); //false

delete person.name;
console.log("name" in person); //true

person.name = "xiaowang";
console.log(person.name); //laowang

使用 Object.defineProperty() 定义新的属性时,一定要为所有特征指定一个值,否则布尔值类型的特征会被默认设置为false。上面这个name就是不可枚举,不可配置,不可写。

定义多重属性

使用 Object.defineProperty() 可以定义一个属性的特征。使用 Object.defineProperties() 可以定义多个属性的特征。

例如

var person = {};

Object.defineProperties(person,{
name:{
value:"laowang",
configurable:true
},
age:{
value:25,
configurable:false,
enumerable:false
}
});

获取属性特征

使用 Object.getOwnPropertyDescriptor() 可以获取一个属性的所有特征

例如

var person = {
name:"laowang"
};

var descriptor = Object.getOwnPropertyDescriptor(person,"name");

console.log(descriptor.enumerable); //true
console.log(descriptor.configurate); //true
console.log(descriptor.writable); //true
console.log(descriptor.value); //laowang

对象禁止扩展

对象也有内部特征,禁止扩展方法是 Object.preventExtensions() 。可以用 Object.isExtensible() 来检查对象的 [[Extensible]] 的值。

例如

var person = {
name:"laowang"
};

console.log(Object.isExtensible(peson)); //true

Object.preventExtensions(peson);
console.log(Object.isExtensible(peson)); //false

person.sayName = function (){
console.log(this.name);
}

console.log("sayName" in person); //false

对象封印

对象封印时创建不可扩展对象的第二种方法。对象被封印后是不可扩展的并且所有属性都不可配置。不仅不能给对象添加新属性,也不能删除属性或改变其类型(从数据属性编程访问器属性或者相反)。一个对象被封印,则只能读写它的属性。

使用 Object.seal() 方法来封印一个对象。对象的 [[Extensible]] 被设置为false。 其余所有属性的 [[Configurate]] 特征被设置为false。可以使用 Object.isSealed() 来判断一个对象是否被封印。

例如

var person = {
name:"laowang"
};

console.log(Object.isExtensible(peson)); //true
console.log(Object.isSealed(peson)); //false

Object.seal(peson);
console.log(Object.isExtensible(peson)); //false
console.log(Object.isSealed(peson)); //true

person.sayName = function (){
console.log(this.name);
}

console.log("sayName" in person); //false

person.name = "xiaowang";
console.log(person.name); //xiaowang

delete person.name;
console.log("name" in person); //true
console.log(person.name); //xiaowang

var descriptor = Object.getOwnPropertyDescriptor(person,"name");

console.log(descriptor.enumerable); //true
console.log(descriptor.configurate); //false

对象冻结

创建不可扩展对象的最后一种方法是冻结对象。如果一个对象被冻结,则不能在对象上添加或者删除属性,不能改变属性类型,也不能写入任何数据属性。被冻结对象是一个数据属性都为只读的封印对象。被冻结的对象无法解冻。使用 Object.freeze() 来冻结一个对象,使用 Object.isFrozen() 来判断一个对象是否被冻结。

例如

var person = {
name:"laowang"
};

console.log(Object.isExtensible(peson)); //true
console.log(Object.isSealed(peson)); //false
console.log(Object.isFrozen(peson)); //false

Object.freeze(peson);
console.log(Object.isExtensible(peson)); //false
console.log(Object.isSealed(peson)); //true
console.log(Object.isFrozen(peson)); //true

person.sayName = function (){
console.log(this.name);
}

console.log("sayName" in person); //false

person.name = "xiaowang";
console.log(person.name); //laowang

delete person.name;
console.log("name" in person); //true
console.log(person.name); //laowang

var descriptor = Object.getOwnPropertyDescriptor(person,"name");

console.log(descriptor.writable); //false
console.log(descriptor.configurate); //false

总结

属性有两种类型:数据属性、访问器属性。数据属性可以保存值,访问器属性不保存值。