JavaScript | 理解对象属性和创建对象的方式
JavaScript 的面向对象编程是比较难的部分,本文目前只针对es5来讲解js的对象,后续会在es6的语法基础上继续补充。欢迎指正
首先,创建自定义对象表示有两种方式
- 创建一个Object的实例
var person = new Object();
person.name = "nice";
person.age = 2;
person.address = "china";
person.sayName = function () {
console.log(this.name);
}
- 使用对象字面量创建
var person = {
name:"hello",
age:12,
address:"shanghai",
sayName:function () {
console.log(this.name);
}
}
现在一般使用第二种方式来创建自定义对象,为啥,因为写起来简单啊老铁们。
上述都创建了一个person对象,其中,name,age,address叫做该对象的属性,sayName显然是该对象的一个方法,返回name
那么下面讲一讲es中的两种属性:数据属性和访问器属性
- 数据属性
1.数据属性包含一个数据值。比如上面的,name的数据值就是hello
2.数据属性有4种特性:
a. configurable 可配置性:能否通过delete删除属性从而重新定义
该属性,能否修改属性的特性,能否将属性改为访问器属性,
默认为true
b. enumerable 可枚举性: 能否通过for in 循环返回属性,默认为
true
c. writable 可修改性。默认为true。
d. value 该属性的数据值。默认为undefined
来几个例子给大家讲一讲这几个属性
- 数据属性configurable默认为true,如果通过Object.defineProperty将某个属性的configurable修改为false,则该属性不能被删除。且再次改回configurabe 为true时会抛出错误
var person = {
name:"hello",
age:12,
address:"shanghai",
sayName:function () {
console.log(this.name);
}
}
person.name = "world";
person.sayName();
// 修改对象的configurable属性 将person对象的name属性变为不可删除的
Object.defineProperty(person,"name",{
configurable:false
})
delete person.name;
person.sayName();
执行结果会打印两遍world,说明将name属性修改为不可配置的时候,delete方法会失效
- 将enumerable修改为false之后,用Object.keys()方法可以列出可枚举属性
// 可枚举性
console.log(Object.keys(person));
Object.defineProperty(person,"name",{
enumerable:false
})
console.log(Object.keys(person));
执行结果
person对象中的name属性已经不可枚举了
- writable 很好理解,修改为false之后就不能改变其数据值
person.sayName();
Object.defineProperty(person,"name",{
writable:false,//person的name属性变为不可更改的
})
person.name = "world";
person.sayName();
试图给person.name赋值,不会生效,打印结果还是hello
- 访问器属性
访问器属性不包含数据值。属性有4个特性,configurable,enumerable,get函数,set函数。一般在设置一个属性的值会导致另一个属性的值发生变化的场景下会使用访问器属性
举个栗子
// 访问器属性
var book = {
_year :2004,
editon:1
}
上述book对象,_year 和 editon 都是数据属性,但是_year这个特殊的命名方式告诉我们,_year是一个只能通过对象方法访问的属性,暗示我们 year 是一个访问器属性。
Object.defineProperty(book,"year",{
get:function () {
return this._year;
},
set:function (newValue) {
if (newValue > 2004){
this._year = newValue;
this.editon = newValue - 2004 + this.editon;
}
}
})
book.year = 2008;
console.log(book.editon);
最后的打印的book.edition的值为5 .上述代码通过set方法在访问器属性的值改变的同时也改变了edition这个数据属性的值
下面讲讲创建对象的N个方法,主要讲3个
构造函数模式
//构造函数来创建对象
function Person(name,age,address) {
this.name = name;
this.age = age;
this.address = address;
this.sayName = function () {
console.log(this.name);
}
// console.log(this);
}
var person1 = new Person("nice",12,"china");
使用构造函数模式创建实例对象,必须要使用new 操作符。构造函数一般为大写字母开头,区别于其他的函数。构造函数中的this指向新对象(即将生成的实例对象)
构造函数的问题就在于,每个方法要在每个实例上重新创建一遍。简而言之,不同实例对象的同名函数是不相等的。
/使用构造函数的同名函数不相等
var person1 = new Person("abc",12,"china");
var person2 = new Person("def",23,"english");
console.log(person1.sayName == person2.sayName);
上述代码的打印结果为 false。
由于构造函数不同实例对象都有不同的方法(尽管方法同名,占据的空间不同),原型模式的诞生就解决了这个问题。
function Animal() {
}
Animal.prototype.name = "cat";
Animal.prototype.age = 12;
Animal.prototype.sayName = function () {
console.log(this.name);
}
var animal1 = new Animal();
var animal2 = new Animal();
console.log(animal1.sayName == animal2.sayName);
上述代码输出结果为 true,说明实例对象animal1和实例对象animal2已经共用了一个方法
我们先来理解一下什么叫原型对象
我们创建了一个函数,那么该函数就存在一个prototype属性,这个属性指向该函数的原型对象。这个原型对象也存在一个属性,叫做constructor,这个属性是一个指针,指回构造函数本身。
然后我们新建了一个实例对象animal1 ,该对象的prototype指针指向它的构造函数的原型对象。
给大家画了一张图:
构造函数和原型对象和实例对象
所以,实例对象其实和构造函数没有直接的关联,与之相关联的是原型对象。
然后我们来看看,js是如何处理实例对象与原型对象之间的关系的。
- 1.生成实例对象,不给实例对象的属性赋值。
function Animal() {
}
Animal.prototype.name = "cat";
Animal.prototype.age = 12;
Animal.prototype.sayName = function () {
console.log(this.name);
}
var animal1 = new Animal();
animal1.sayName(); // 打印 cat
- 2.给实例对象的属性赋值
var animal1 = new Animal();
animal1.name = "rubbit";
animal1.sayName(); // 打印 rubbit
此时,实例对象的name属性被赋值为rubbit,但是原型对象中name属性的值仍然是 cat
- 3.原型对象中存在引用类型属性,多个实例对象共享了该引用类型属性
animal1.friends.push("fish","duck");
console.log(animal1.friends); // ["dog","bird","fish","duck"]
var animal2 = new Animal();
console.log(animal2.friends); //["dog","bird","fish","duck"]
animal1实例对象对引用类型属性的改变,同时被animal2共享了。在某些时候,这不是我们想要的结果
组合构造函数和原型模式
上文已经说了,构造函数模式会造成空间的浪费,原型模式的属性被所有实例对象共享,都有各自的缺点。那么组合模式就解决了这两个问题。
构造函数用于定义实例属性,该属性为实例独有,接收传参,原型模式用于定义方法和实例共享的属性,取两者之长。
// 构造函数模式
function Person(name,age,address) {
this.name = name;
this.age = age;
this.address = address;
this.friends= ["bob","nicy"];
}
// 原型模式
Person.prototype = {
constructor : Person,
sayName:function () {
console.log(this.name);
}
}
var person1 = new Person("cat",12,"eng");
var person2 = new Person("dog",13,"cha");
person1.sayName(); //cat
person2.sayName(); // dog
console.log(person1.sayName === person2.sayName); //true
console.log(typeof person1); // Object
person1.friends.push("van");
console.log(person1.friends); // ["bob","nicy","van"]
console.log(person2.friends); // ["bob","nicy"]
当然还有寄生构造函数模式,稳妥构造函数模式等不太常用的方式,有兴趣大家可以自己去了解一下。
好了,到这里,什么是对象,以及在不同情况下如果创建对象已经基本讲完了。简单地说,构造函数,原型和实例之间的关系就是:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例对象都包含一个指向原型对象的内部指针。
好了,旁友们,下一篇我们讲继承。