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

4086846825

原型对象链和 Object.prototype

js內建的继承方法被称为原型对象链,又可以称为原型对象继承。原型对象的属性可以经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象,他也有自己的原型对象并继承其属性。这就是原型对象链,而原型对象继承它的原型对象,以此类推。

所有对象,包括哪些你自己定义的对象都自动继承自 Object,除非另有指定。更确切的说,所有对象都继承自 Object.prototype。任何以队形字面量形式定义的对象,其 [[Prototype]] 的值都被设为 Object.prototype,这意味着它继承 Object.prototype 的属性。

var book = {
title:"javascript"
};

var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); /true

继承自 Object.peototype 的方法

前面说到的很多方法其实都是定义在 Object.prototype 上面的。因此可以被其他对象继承。

方法 作用
hasOwnProperty() 检查是否存在一个给定名字的自有属性
propertyIsEnumerable() 检查一个自有属性是否可枚举
isPrototypeof() 检查一个对象是否是另一个对象的原型对象
valueOf() 返回一个对象的值表达
toString() 返回一个对象的字符串表达

valueOf,每当一个操作符被用于一个对象时就会调用 valueOf() 方法。 valueOf() 默认返回对象实例本身。原始封装类型重写了 valueOf() 以使得它对 String 返回一个字符串,对 Boolean 返回一个布尔值,对 Number 返回一个数字。类似的,Date 对象返回一个 epoch 时间。如果你的对象也要这样使用操作符,也可以自定义 valueOf() 方法.定义的时候并没有改变操作符的行为,仅仅定了操作符默认行为所使用的值。

toString(),一旦 valueOf() 方法返回的是一个引用而不是原始值的时候,就会回退调用 toString() 方法。另外,当js期望一个字符串时,也会对原始值隐式调用 toString() .例如,当加号操作符的一边是一个字符串时,另一边会被自动转换成字符串。如果另一边是一个原始值,会自动被转换成一个字符串表达(例如,true 转换成 “true”)。如果另一边是一个引用值,则会调用 valueOf()。如果 valueOf() 返回一个引用值,则调用 toString()。

例如

var book = {
title:"javascript"
};

var message = "Book = " + book;
console.log(message); /"Book = [object Object]"

这段以 “Book =” 和book来构造字符串。因为book是一个对象,此时调用它的 toString() 方法。该方法继承自 Object.prototype,大部分js引擎返回默认值 “[object Object]”。如果对这个值满意,就不需要改变对象的 toString() 方法。定义自己的 toString() 方法有时候可以为此类字符串转换提供更过信息的值。

var book = {
title:"javascript",
toString:function(){
return "[Book " + this.title + "]";
}
};

var message = "Book = " + book;
console.log(message); /"Book = [Book javascript]"

修改 Object.pototype

所有的对象都默认继承自 Object.prototype,所以改变 Object.prototype 会影响所有的对象,是非常危险的。

例如

Object.prototype.add = function(value){
return this + value;
}

var book = {
title:"javascript"
};

console.log(book.add(5)); /"[object Object]5";
console.log("title".add("end")); /"titleend"

/在浏览器中
console.log(document.add(true)); /"[object HTMLDocument]true"
console.log(window.add(5)); /"[object Window]6"

上面给 Object.prototype 添加方法可能会带来不可预知的结果

例如

var empty = {};

for(var prpperty in empty){
console.log(property);
}
/ add

空对象依然会输出一个”add”属性。考虑到js中 for-in 使用频繁,为 Object.prototype 添加可枚举属性会影响大量代码。所以可以在 for-in 中使用 hasOwnProperty().

例如

var empty = {};

for(var prpperty in empty){
if(empty.hasOwnProperty(property)){
console.log(property);
}
}
/

这样循环只会输出对象自有属性,不会输出原型属性。

对象继承

对象继承是最简单的继承类型。唯一需要做的就是制定那个对象是新对象的 [[Prototype]] 。对象字面量形式会隐式指定 Object.prototype 为其 [[Prototype]],也可以使用 Object.create() 方法显式指定。

例如

var book = {
title:"javascript"
};

/和下面是一样的

var book = Object.create(Object.prototype,{
title:{
configurable:true,
enumerable:true,
value:"javascript",
writable:true
}
});

继承其他对象

var person1 = {
name:"laowang",
sayName:function(){
console.log(this.name);
}
};

var person2 = Object.create(person1,{
name:{
configurable:true,
enumerable:true,
value:"xiaowang",
writable:true
}
});

person1.sayName(); /"laowang"
person2.sayName(); /"xiaowang"

console.log(person1.hasOwnProperty("sayName")); /true
console.log(person1.isPrototypeOf(person2)); /true
console.log(person2.hasOwnProperty("sayName")); /false

另外,也可以通过 Object.create() 创建 [[Prototype]] 为null的对象,这样的对象是没有原型对象链的对象。意味着 toString() 和 valueOf() 等內建方法都不存在该对象上面。实际上,这种对象完全就是一个没有任何预定义属性的白板,也是一个完美的哈希容器,因为不会有自由属性和原型属性的冲突。

例如

var nakedObject = object.create(null);

console.log("toString" in nakedObject); /false
console.log("valueOf" in nakedObject); /false

构造函数继承

js中的对象继承也是构造函数继承的基础。几乎所有的函数都有 prototype 属性,它可以被修改或者换。该 prototype 属性被自动设置为一个新的继承自 Object.prototype 的泛用对象,该对象有一个自有属性 constructor ,实际上js引擎自动做了下面的事情

function YourConstructor(){

}

/js引擎自动为你做了下面的事情
YourConstructor.prototype = Object.create(Object.prototype,{
constructor:{
configurable:true,
enumerable:true,
value:YourConstructor,
writable:true
}
});

创建出来的对象都继承自 Object.prototype。YourConstructor 是Object的子类,Object是YourConstructor 的父类。

由于 prototype 属性可写,可以通过改写它来改变原型对象链。

例如

function Rectangle(length,width){
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function(){
return this.length * this.width;
};

Rectangle.prototype.toString = function(){
return "[Rectangle " + this.length + "X" + this.width + " ]";
};

/继承Reactangle
function Square(size){
this.length = size;
this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constuctor = Square;
Square.prototype.toString = function(){
return "[Square " + this.length + "X" + this.width + " ]";
};

 

var rect = new Rectangle(5,10);
var square = new Square(6);

console.log(rect.getArea()); /50
console.log(square.getArea()); /36

console.log(rect.toString()); /"[Rectangle 5X10 ]"
console.log(square.toString()); /"[Square 6X6 ]"

console.log(rect instanceof Rectangle); /true
console.log(rect instanceof Object); /true

console.log(square instanceof Square); /true
console.log(square instanceof Rectangle); /true
console.log(square instanceof Object); /true

此时不需要给Rectangle的调用提供参数,因为他们不需要被使用,而且如果提供了,那所有Square的对象实例都会共享同样的维度。用这种方式改变原型对象链时,需要确保构造函数不会再参数缺失时抛出错误(很多构造函数包含的初始化逻辑会需要参数)且构造函数不会改变任何全局状态,比如追踪有多少实例被创建等。

rect作为Rectangle的实例被创建,而square则是作为Square的实例被创建。两个对象都有getArea方法,那是因为继承自Reatangle.prototype。instanceof操作符认为变量square同时是Square、Rectangle、Object的实例,因为instanceof是使用原型对象链检查对象类型。

使用 Object.create() 方法可以简化并且不会导致参数缺失而报错。

例如

function Rectangle(length,width){
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function(){
return this.length * this.width;
};

Rectangle.prototype.toString = function(){
return "[Rectangle " + this.length + "X" + this.width + " ]";
};

/继承Reactangle
function Square(size){
this.length = size;
this.width = size;
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
return "[Square " + this.length + "X" + this.width + " ]";
};

 

var rect = new Rectangle(5,10);
var square = new Square(6);

console.log(rect.getArea()); /50
console.log(square.getArea()); /36

console.log(rect.toString()); /"[Rectangle 5X10 ]"
console.log(square.toString()); /"[Square 6X6 ]"

console.log(rect instanceof Rectangle); /true
console.log(rect instanceof Object); /true

console.log(square instanceof Square); /true
console.log(square instanceof Rectangle); /true
console.log(square instanceof Object); /true
````

## 构造函数窃取

由于js中的继承是通过原型对象链来实现的,因此不需要调用对象的父类构造函数。如果需要在子类构造函数中调用父类构造函数,就需要用 ___call()___ 或者 ___apply()___。

例如
```javascript
function Rectangle(length,width){
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function(){
return this.length * this.width;
};

Rectangle.prototype.toString = function(){
return "[Rectangle " + this.length + "X" + this.width + " ]";
};

/继承Reactangle
function Square(size){
Retangle.call(this,size,size);
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
return "[Square " + this.length + "X" + this.width + " ]";
};

 

var square = new Square(6);

console.log(square.length); /6
console.log(square.width); /6
console.log(square.getArea()); /36

由于这种做法模仿了那些基于类语言的类继承,通常被称为伪类继承。

访问父类方法

子类提供的新功能覆盖父类方法很常见,但是如果还想访问父类方法,只能通过 call() 或者 apply() 来访问了,而且这是唯一方法。

例如

function Rectangle(length,width){
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function(){
return this.length * this.width;
};

Rectangle.prototype.toString = function(){
return "[Rectangle " + this.length + "X" + this.width + " ]";
};

/继承Reactangle
function Square(size){
Retangle.call(this,size,size);
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
var text = Rectangle.prototype.toString().call(this);
return text.replace("Rectangle","Square");
};

总结

js通过原型对象链支持继承。当将一个对象的 [[Prototype]] 设置为另一个对象时,就在这两个对象之间创建了一条原型对象链。所有的泛用对象都自动继承自 Object.prototype 。如果你想创建一个继承自其它对象的对象,你可以用 Object.create() 指定 [[Prototype]] 为一个新对象。

 

可以在构造函数中创建原型对象链来完成自定义类型之间的继承。通过将构造函数的 prototype 属性设置为某一个对象那个,就建立了自定义类型对象和该对象的继承关系。构造函数的所有对象、实例共享同一个原型对象,所以他们都继承自该对象。这个技术在继承其他对象的方式时工作得很好。但是不能用原型继承自有属性。

为了正确继承自有属性,可以使用构造函数窃取。只需要以 call() 或者 apply() 调用父类的构造函数,就可以在子类里面完成各种初始化。结合构造函数窃取和原型对象链是js中最常见的继承手段。由于和基于类的继承相似,这个组合经常被称为伪类继承。

可以通过直接访问父类原型对象的方式访问父类方法。必须以 call() 或者 apply() 执行父类方法并传入一个子类的对象。

JavaScript面向对象系列:四、构造函数和原型对象

[[Prototype]] 属性

一个对象实例通过内部属性 [[Prototype]] 跟踪其原型对象。该属性是一个指向该实例使用的原型对象的指针,当你new创建一个新的对象时,构造函数的原型对象会被赋值给该对象的 [[Prototype]] 属性。

可以调用对象的 Object.getPrototypeOf() 方法读取[[Prototype]] 属性的值。

var obj = {};
var prototype = Object.getPrototypeOf(obj);
console.log(prototype === Object.prototype); /true

大部分js引擎都会提供一个 __ proto __属性。该属性使你可以直接读写 ___[[Prototype]]___ 属性。

可以使用 isPrototypeOf() 方法检查某个对象是否是另一个对象的原型对象,该方法在所有对象中都有.

var obj = {};
console.log(Object.prototype.isPrototypeOf(obj)); /true

因为obj是一个泛用对象,原型是Object.prototype

当读取一个对象的属性时,js引擎首先在该对象的自有属性中查找属性名。如果找到则返回。如果自有属性中不包含该名字,则js引擎会搜索[[Prototype]] 中的对象,如果找到则返回。如果找不到,则返回undefined。

var obj = {};
console.log(obj.toString()); /"[object Object]"

obj.toString = function(){
return "[object Custom]";
}
console.log(obj.toString()); /"[object Custom]"

delete obj.toString;
console.log(obj.toString()); /"[object Object]"

delete obj.toString;
console.log(obj.toString()); /"[object Object]"

上述例子也说明了,仅当自有属性被删除时,原型属性才会再一次被使用。delete操作符仅对自有属性起作用,无法删除原型对象的属性。

在构造函数中使用原型对象

使用对象字面量形式改写原型对象时要注意 constructor 属性

例如

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

Person.prototype = {
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person " + this.name + "]";
}
}

var person1 = new Person("laowang");

console.log(person1 instanceof Person); /true
console.log(person1.constructor === Person); /false
console.log(person1.constructor === Object); /true

使用这种方式创建原型对象,constructor 属性会有误差。每一个原型对象都有一个 constructor 属性,这是其他对象实例没有的。

当一个函数被创建时,它的 prototype 属性也被创建,且该原型对象的 constructor 属性指向该函数。当使用对象字面量形式改写原型对象的时候,constructor 被置为泛对象Object。得手动修正。

例如

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

Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person " + this.name + "]";
}
}

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

console.log(person1 instanceof Person); /true
console.log(person1.constructor === Person); /true
console.log(person1.constructor === Object); /false

console.log(person2 instanceof Person); /true
console.log(person2.constructor === Person); /true
console.log(person2.constructor === Object); /false

构造函数、原型对象和对象实例之间的关系最有趣的一个方面也许就是对象实例和构造函数之间没有直接联系。不过对象实例和原型对象以及原型对象和构造函数之间都有直接联系。

改变原型对象

给定类型的所有对象实例功效一个原型对象,所以可以一次性扩充所有对象实例。_[[Prototype]] 属性只是包含了一个指向原型对象的指针。任何对原型对象的改变都立即反映到所有引用它的对象实例上。

例如

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

Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person " + this.name + "]";
}
}

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

console.log("sayHi" in person1); /false
console.log("sayHi" in person2); /false

Person.prototype.sayHi = function(){
console.log("Hi");
};

person1.sayHi(); /"Hi"
person2.sayHi(); /"Hi"

对象封印和对象冻结只能操作对象的自有属性。封印或者冻结之后,无法添加自有属性或者改变冻结对象的自有属性。

例如

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

Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
},
toString:function(){
return "[Person " + this.name + "]";
}
}

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

Object.freeze(person1);

console.log("sayHi" in person1); /false
console.log("sayHi" in person2); /false

Person.prototype.sayHi = function(){
console.log("Hi");
};

person1.sayHi(); /"Hi"
person2.sayHi(); /"Hi"

其实,[[Prototype]] 属性是对象实例的自有属性,属性本身被冻结,但是其指向的值(原型对象)并没有被冻结。

实际开发中,可能不会频繁的使用原型对象,但是理解对象实例及其原型对象之间的关系是非常重要的。

改变内建原型对象

所有的內建对象都有构造函数,因此也都有原型对象可以改变。

例如,简单修改Array的原型对象

Array.prototype.sum = function(){
return this.reduce(function(pre,cur){
return pre + cur;
});
};

var numbers = [1,2,3,4,5];
var result = numbers.sum();

console.log(result); /15

总结

构造函数就是用 new 操作符调用的普通函数。可以随时定义自己的构造函数来创建多个具有同样属性的对象。可以用 instanceof 操作符或直接访问 constructor 属性来鉴别对象是被哪个构造函数创建的。

每一个函数都有都具有 prototype 属性,它定义了该构造函数创建的所有对象共享的属性。通常,共享的方法和原始值属性被定义在原型对象里,而其他属性都定义在构造函数里。 constructor 属性实际上被定义在原型对象里供所有对象实例共享。

原型对象被保存在对象实例内部的 [[Prototype]] 属性中。这个属性时一个引用而不是副本。由于js查找属性的机制,对原型对象的修改都立刻出现在所有对象实例中。当试图访问一个对象的某个属性时,js首先在自有属性里查找该名字,如果自有属性中没有找到则在原型属性中查找。这样的机制意味着原型对象可以随时改变而引用它的对象实例则立即反映出这些改变。

內建对象也有可以被修改的原型对象。

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。

(870) 435-9596

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会在背后创建这些对象是的你能够像使用普通对象那样直接使用原始值,但是这些临时对象在使用它们的语句结束时就立刻被销毁。虽然你也可以自己创建原始封装类型的实例,但是它们太容易令人误解,所以最好别这么干。

我们为什么需要redux

1.1.1 redux是什么

通俗的来讲,redux就是一个state管理库

1.1.2 不使用redux构建应用

  1. 一般构建的React组件内部可能是一个完整的应用,它自己工作良好,你可以通过属性作为API控制它。但是更多的时候发现React根本无法让两个组件互相交流,使用对方的数据。然后这时候不通过DOM沟通(也就是React体制内)解决的唯一办法就是提升state,将state放到共有的父组件中来管理,再作为props分发回子组件。
  2. 子组件改变父组件state的办法只能是通过事件触发父组件声明好的回调,也就是父组件提前声明好函数或方法作为契约描述自己的state将如何变化,再将它同样作为属性交给子组件使用。这样就出现了一个模式:数据总是单向从顶层向下分发的,但是只有子组件回调在概念上可以回到state顶层影响数据。这样state一定程度上是响应式的。
  3. 为了面临所有可能的扩展问题,最容易想到的办法就是把所有state集中放到所有组件顶层,然后分发给所有组件。这样就造成了很多中间组件为了传递props而增加一些冗余的属性。
  4. 最重要的是父组件与子组件的通信,会造成数据的重复,带来的一个问题是如何保证数据重复的数据一致,如果数据存储多份而且不一致,就很难决定到底使用哪个数据作为正确结果了。
  5. 对于数据重复的问题,一致很直观的解决方法就是以某一个组件的状态为准,这个组件是状态的”领头羊”,其余组件都保持和”领头羊”的状态同步,但是实际情况下这种方法可能难以实施。
  6. 另一种思路是,干脆不要让任何一个react组件扮演”领头羊”角色,把数据源放在react组件之外形成全局状态。这便是redux的store,全局唯一的数据源。

1.1.3 redux为我们做了什么

image

1.1.4 按照redux思想来设计

redux真正的灵魂在其设计思想,很多时候我们可能并不需要redux库本身,我们可以尝试着来应用其思想.

例如:使用组件内state的计数器

import React, { Component } from 'react';

class Counter extends Component {
state = { value: 0 };

increment = () => {
this.setState(prevState => ({
value: prevState.value + 1
}));
};

decrement = () => {
this.setState(prevState => ({
value: prevState.value - 1
}));
};

render() {
return (
<div>{this.state.value}
<button>+</button>
<button>-</button></div>
)
}

应用redux思想的计数器

import React, { Component } from 'react';

const counter = (state = { value: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { value: state.value + 1 };
case 'DECREMENT':
return { value: state.value - 1 };
default:
return state;
}
}

class Counter extends Component {
state = counter(undefined, {});

dispatch(action) {
this.setState(prevState => counter(prevState, action));
}

increment = () => {
this.dispatch({ type: 'INCREMENT' });
};

decrement = () => {
this.dispatch({ type: 'DECREMENT' });
};

render() {
return (
<div>{this.state.value}
<button>+</button>
<button>-</button></div>
)
}
}

1.1.5 用redux的好处

  1. 前端开发state可控。
  2. 前端数据结构统一管理。
  3. 数据流向单一,团队开发互相影响较小。

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() 方法来检查一个属性是否为可枚举,每个对象都有这个方法。

访问器属性

get 和 set 属性时访问器属性,可以定义属性被读取或者写入时候的行为。这两个属性可以只设置其中一个,如果只设置 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

总结

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