ECMA-262 定义了只有内部才用的特性,这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为了表示特性是内部值,使用
[[ ]]
ECMAScript 有两种属性:数据属性 和 访问器属性。
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。
数据属性有 4 个描述其行为的特性:
- [[Configurable]] —— 能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性或者把属性修改为访问器属性。默认值 true。
- [[Enumerable]] —— 能否通过 for-in 循环返回属性。默认值 true。
- [[Writable]] —— 能否修改属性的值。默认值 true。
- [[Value]] —— 包含这个属性的数据值。读取属性值时从这个位置读;写入属性值时写入这个位置。默认值undefined。修改属性默认的特性,使用
object.defineProperty()
方法:
object.defineProperty(属性所在的对象,属性的名字,一个描述符对象)
描述符对象的属性必须是:Configurable、Enumerable、Writable、Value
var person = {};
object.defineProperty(person,"name",{
writable:false,
name:"Nicholas"
});
注意:一旦把属性定义为不可配置的,就不能再把它变回可配置的了。
不包含数据值,包含一对 getter 和 setter 函数(非必需的)
读取访问器属性时,调用 getter 函数返回有效的值;写入访问器属性时,调用 setter 函数并传入值。
访问器属性有如下4个特性:
- [[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(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
只指定 getter 表示属性不能写;只指定 setter 表示属性不能读
object.definePropertise()
接收两个对象参数:
- 第一个对象是要添加修改的其属性的对象
- 第二个对象的属性与第一个对象的属性要一一对应
在 book 对象上定义了两个数据属性和一个访问器属性
var book = {};
object.definePropertise(book,{
_year: {
writable:true,
value:2004
},
edition: {
writable:true,
value:1
},
year: {
get:function(){
return this._year;
},
set: function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
object.getOwnPropertyDescriptor()
获取给定属性的描述符
接收两个参数:属性所在的对象和读取其描述符的属性名称。返回一个对象。
var desp = object.getOwnPropertyDescriptor(book,"_year");
alert(desp.value);//2004
alert(desp.configurable);//false
alert(desp.get);//undefined
var desp = object.getOwnPropertyDescriptor(book,"year");
alert(desp.value);//undefined
alert(desp.configurable);//false
alert(desp.get);//"function"
JavaScript中,对于任何对象(包括BOM、DOM对象)使用该方法
object构造函数或对象字面量都可以用来创建单个对象,但有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码。
这种模式抽象了创建具体对象的过程
ECMAScript中无法创建类,可以用函数来封装以特定接口创建对象的细节。
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 person = createPerson("Greg",27,"Doctor");
优缺点:解决了创建多个相似对象的问题,但没有解决对象识别问题,即如何知道对象的类型。
ECMAScript中的构造函数可用来创建特定类型的对象
创建自定义的构造函数,定义自定义对象类型的属性和方法
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("Greg",27,"Doctor");
var person2 = new Person("Tom",19,"IT");
alert(person1.constructor == Person);//true
alert(person2.constructor == Person);//true
//检测对象类型
alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
构造函数始终都应该以一个大写字母开头,非构造函数以小写字母开头
使用new
操作符创建 Person 的新实例。这种方式调用构造函数经历以下4个步骤:
1)创建一个新对象
2)将构造函数的作用域赋给新对象(因此this指向这个新对象)
3)执行构造函数中的代码(为这个新对象添加属性)
4)返回新对象对象的
constructor
(构造函数)属性,用来标识对象类型的。上面的两个对象的constructor
属性指向Person。
构造函数和普通函数的唯一区别在于调用方式不同:只要通过
new
操作符来调用,就可以作为构造函数;否则不是。
//当做构造函数使用
var person = new Person("Nicholas",20,"SE");
person.sayName();
//在全局作用域中调用
Person("Nicholas",20,"SE");
window.sayName();
//在另一个对象的作用域中
var o = new Object();
Person.call(o,"Nicholas",20,"SE");
o.sayName();
优缺点:可以知道对象的类型,但是每个方法都要在每个实例上重新创建一遍。
不同实例的同名函数是不相等的
alert(person1.sayName == person2.sayName);//false
创建两个完成同样任务的Function实例是没有必要的。
创建的每个函数都有一个
prototype
(原型)属性,这个属性是一个指针,指向一个对象(函数的原型对象)。而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
prototype
是通过调用构造函数而创建的那个对象的原型对象。
原型对象的好处:所有对象的实例共享它所包含的属性和方法。不必在构造函数中定义对象实例的信息,可以直接添加到原型对象中,例如:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age= 29;
Person.prototype.job= "SE";
Person.prototype.sayName= function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName();//"Nicholas"
var person2 = new Person();
person2.sayName();//"Nicholas"
alert(person1.sayName == person2.sayName);//true
与构造函数模式不同的是,新对象的属性和方法是由所有实例共享的。
创建了一个新函数,就会为该函数创建一个
prototype
属性,指向该函数的原型对象。
所有原型对象都会自动获得一个constructor
(构造函数)属性,指向prototype
属性所在的函数。Person.prototype.constructor 指向 Person。当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性[[Prototype]]),指向构造函数的原型对象
注意:这种连接存在于实例与构造函数的原型对象之间。这两个实例与构造函数没有直接的关系。
*
在实现中无法访问到[[Prototype]],但可以通过以下方式
isPrototypeOf()
——确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true
Object.getPrototypeOf()
——获取一个对象的原型。返回[[Prototype]]的值
alert(Object.getPrototypeOf(person1)==Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//"Nicholas"
*
在调用 person1.sayName() 时,会先后执行两次搜索:
1)首先从对象实例开始。在实例中找到了就返回该属性的值。
2)如果没有找到,则继续搜索,搜索指针指向的原型对象。在原型对象中找到了就返回该属性的值。
*
虽然可以通过对象实例访问保存在原型中的值,却不能通过对象实例重写原型中的值
如果在对象实例中添加了一个与实例原型中的同名属性,那么就在实例中创建 该属性,该属性将会屏蔽原型中的那个属性,例如:
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name);//"Greg"——来自实例
alert(person2.name);//"Nicholas"——来自原型
delete person1.name;
alert(person1.name);//"Nicholas"——来自原型
添加的这个属性,只会阻止我们访问原型中的那个属性,但不会修改那个属性
使用delete
可以完全删除实例属性,从而可以重新访问原型中的属性。
hasOwnProperty()
——检测一个属性是存在于实例中,还是存在于原型中
存在于对象实例中时才返回true
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name"));//false 原型属性
person1.name = "Greg";
alert(person1.name);//"Greg"——来自实例 实例属性
alert(person1.hasOwnProperty("name"));//true
使用 in 操作符的两种方式:单独使用和 在 for-in 循环中使用
1)单独使用
在对象能够访问给定属性时返回true
,无论是存在于实例中还是原型中
var person1 = new Person();
alert(person1.hasOwnProperty("name"));//false 原型属性
alert("name" in person1);//true
person1.name = "Greg";
alert(person1.name);//"Greg"——来自实例 实例属性
alert(person1.hasOwnProperty("name"));//true
alert("name" in person1);//true
2)在使用 for-in 循环时,返回的是所有能通过对象访问的、可枚举的属性
object.keys()
——获取对象上所有可枚举的实例属性
接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组
var keys=object.keys(Person.prototype);
alert(keys);//"name,age,job,sayName"
object.getOwnPropertyNames()
——获取对象上所有实例属性,无论是否可枚举
var keys=object.getOwnPropertyNames(Person.prototype);
alert(keys);//"constructor,name,age,job,sayName"
包含一个不可枚举的属性
constructor
用一个包含所有属性和方法的对象字面量形式来重写整个原型对象.
function Person(){}
Person.prototype = {
name: "Nicholas",
age: 29,
job: SE,
sayName: function(){
return alert(this.name);
}
}
最终结果相同,但是
constructor
属性不再指向 Person 了。
因为这里的语法本质上完全重写了默认的 prototype 对象,因此constructor
属性也就变成了新对象的constructor
属性(指向 Object 构造函数),constructor
已经无法确定对象的类型了
var friend = new Person();
alert(friend.constructor == Person);//false
alert(friend.constructor == Object);//true
如果需要可以特意将它设置成适当的值,重设constructor
属性
function Person(){}
Person.prototype = {
constructor: Person;
...
}
对原型对象所做的任何修改都能够立即从实例中反映出来。
即使先创建的实例后修改的原型也是如此。因为实例与原型之间的链接只是一个指针,而非一个副本。因此可以随时为原型添加属性和方法。
但是如果重写了整个原型对象,把原型修改为另一个对象等于切断了构造函数与最初原型之间的联系。
function Person(){}
var friend = new Person();
Person.prototype = {
name: "Nicholas",
age: 29,
job: SE,
sayName: function(){
return alert(this.name);
}
}
friend.sayName();//error,因为friend指向的原型不包含改属性
重写原型切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
所有原生引用类型(Object,Array,String
等等)都是采用原型模式创建的,在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,也可以定义新的方法。
可以像修改自定义对象的原型一样修改原生对象的原型
alert(typeof Array.prototype.sort);//"function"
//定义新方法
String.prototype.startWith = function(text){
return this.indexOf(text) == 0;
}
var msg="Hello world!";
alert(msg.startWith("Hello"));//true
优点:可以创建自定义类型;所有对象的实例共享它所包含的属性和方法。
缺点:省略了为构造函数传递初始化参数,结果所有实例默认情况下都取相同的属性值;原型模式最大的问题是由其共享的本质所导致的。
当原型中包含引用类型值的属性时,对实例person1.friends
的修改也会通过person2.friends
反映出来。
所有很少单独使用原型模式
function Person(){}
Person.prototype = {
...
friends: ["Sheiby","Court"],
...
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);//"Sheiby,Court.Van"
alert(person2.friends);//"Sheiby,Court.Van"
这种方式是创建自定义类型最常见的方式。
构造函数模式:用于定义实例属性;原型模式用于定义方法和共享的属性。这样,每个实例都会有自己的一份实例属性的副本,同时又共享着对方法的引用
支持向构造函数传递参数
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Sheiby","Court"]
}
Person.prototype = {
constructor: Person,
sayName: function(){
return alert(this.name);
}
}
var person1 = new Person("Nicholas",29,"SE");
var person2 = new Person("Greg",27,"Doctor");
person1.friends.push("Van");
alert(person1.friends);//"Sheiby,Court,Van"
alert(person2.friends);//"Sheiby,Court"
将所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
通过检查某个应该存在的方法是否有效,来决定是否初始化原型。
function Person(name,age,job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
如果
sayName()
方法不存在,才会将它添加到原型中。
创建一个函数,封装创建对象的代码,然后再返回新创建的对象
function Person(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 friend= new Person("Greg",27,"Doctor");
除了使用
new
操作符并把创建的函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的
说明:返回的对象与构造函数或与构造函数的原型属性之间没有关系。
因此不能依赖instanceof
来确定对象的类型
稳妥对象指的是没有公共属性,而且其方法也不引用 this 的对象。
适用场景:安全环境中(禁止使用 this 和 new);防止数据被其他应用程序改动时;
稳妥构造函数与寄生构造函数类似的模式,但有两点不同:
1)新创建对象的实例方法不引用this
;
2)不使用new
操作符调用构造函数。
function Person(name,age,job){
var o = new Object();
//可以在这里定义私有变量和函数
o.sayName = function(){
alert(name);
}
return o;
}
var friend= Person("Greg",27,"Doctor");
friend.sayName();
变量
friend
保存的是一个稳妥对象,除了调用sayName()
方法外,没有别的方式访问其数据成员.
这种模式提供的安全性,使它非常适合某些安全执行环境
注意:与寄生构造函数模式类似,使用这种模式创建的对象与构造函数没有什么关系,因此也不能使用instanceof
来确定对象的类型