面向对象的语言都有一个类的概念,通过这种类的概念,我们能创建出具有相同属性相同方法的对象。
ECMA-262将对象定义为: 无序属性的集合,其属性可以包含基本值、对象或者函数。
可以理解为对象是一个没有序号的数组,他的“排序方式“是依靠键值(key&value)组合的。
那么我们就开始说说面向对象开发吧( 下面的代码都是在同一个文件当中写的,我不想重复代码 )
1. 什么是对象,我用下面这个例子来演示一下:
// 创建对象(第一种)
var dog1 = new Object();
dog1.name = '旺财';
dog1.age = 2;
dog1.color = 'brown';
dog1.bark = function () {
console.log('wang wang wang!!!');
};
// 对象字面量创建对象(第二种)
var dog2 = {
name: '小泰迪',
age: 1,
color: 'brown',
bark: function () {
console.log('wang wang wang!!!');
}
};
在上面,我在第一种方式创建了一个dog1的空对象,并且分别给它赋予了 name age color 属性 和一个bark的方法。
如果你觉得这种方法比较麻烦,就用第二种方式创建,最后的效果都是一样的。
但是我们在一般开发中如果单单使用这种简单的属性是不够的,ECMA-262给我们提供了可以描述这些属性的特性的方法:
Object.defineProperty()
它可插入三个参数,分别是 对象, 属性, 设置的特性对象, 我们用一个例子展示:
Object.defineProperty(dog2, 'name', {
writable: false, // 是否可写, 默认true
value: '大泰迪', // 属性值
enumerable: true, // 是否可遍历, 默认true
configurable: true // 是否可被设置, 默认true
});
我们在这个例子中, 让dog2 的 name 属性:
值不可更改 (do2.name = '' 会没效果),
但是更改了它的值为 大泰迪(这里的配置与前面不冲突),
可遍历(我们可以在for循环, 数组的函数遍历中可以获取),
可再配置(如果为false,则如果在后续中想修改配置则报错)
如果想同时配置多个属性:
Object.defineProperties
Object.defineProperties(dog2, {
name: {
writable: false, // 是否可写
value: '大泰迪', // 属性值
},
age: {
value: 2
},
color: {
value: 'white'
}
});
既然可以定义属性的特性, 那应该也可以获取属性的特性,没错, 真的有:
Object.getOwnPropertyDescriptor(obj, key)
接下来到了创建对象了, 上面创建对象是最简单的, 但是复杂的程序当中, 这种方式是不可靠的
工厂模式: 这种模式是一种比较广为人知的设计模式(我目前还没仔细去研究设计模式的东西, 这个东西比较虚, 但以后有空肯定会去细究的)。它抽象出创建具体对象的过程。
//工厂模式
function createDog(name, age, color) {
var obj = {};
obj.name = name;
obj.age = age;
obj.color = color;
obj.bark = function () {
console.log('wangwangwang !!!!')
}
return obj;
}
var dog3 = createDog('小黑', 1, 'black');
构造函数模式: 构造函数可以用来创建特定类型的对象, 像 Object和Array这样的原生构造函数,在运行时是会自动出现在执行环境当中的
// 构造函数模式
function Dog(name, age, color) {
this.name = name;
this.age = age;
this.color = color;
this.bark = function () {
console.log('wangwangwang !!!!')
}
}
var dog4 = new Dog('小白', 2, 'white');
我们在这个例子当中使用到了 new 操作符 , new的操作过程是如下:
1.创建新对象
2.将构造函数的作用域赋值给新的对象(this指向新对象)
3.执行构造函数的代码(为dog4添加属性)
4.返回新对象
那既然有了构造函数,我们如何知道这个对象就是某个构造函数new 的呢?
instanceof 操作符可以帮忙, 它能告诉你 这个对象是否是由这个构造函数生成的
console.log(dog4 instanceof Dog); // true
console.log(dog4 instanceof Object); // true
上面 dog4 也是 Object的实例 , 后面讲到原型链会讲到
既然它叫做构造函数,说明它其实也是一个函数, 并不存在有什么特殊的语法 那当我们用普通函数调用它的时候会怎样呢
Dog('小红', 1, 'red'); // 因为是在全局环境下调用的, 所以this会指向window, 函数调用添加的属性都到了window中
console.log(window.name); // 小红
console.log(window.age); // 1
console.log(window.color); // red
这里由于涉及到this, this 的用法可以到我的文章中学习: this 的使用
你要说构造函数厉害,这是不可否认的,但一样东西肯定有它的优缺点的
我们每次调用这个构造函数的时候,里面的属性在每个实例中都需要去重新创建, 到了大型的程序中,这样的构造函数使用是不太合理的
按照我这样说,肯定有人会这样去做
function Dog(name, age, color) {
this.name = name;
this.age = age;
this.color = color;
this.bark = bark
}
function bark() {
console.log('wangwangwang!!!');
}
但是其实将 bark 定义到外面调用,虽然看上去解决了, 但是如果我们的属性需要用到很多方法, 将其全部放到全局环境是否合理, 这一点都不优雅!
原型模式:
虽然上面的方法解决不了,但好在每个函数创建的时候都有一个prototype 的属性, 这个属性是一个指针, 指向一个对象
function Dog() {
//
}
Dog.prototype.name = '小红';
Dog.prototype.age = 2;
Dog.prototype.color = 'red';
Dog.prototype.bark = function () {
console.log('wangwangwang!!!');
};
var dog5 = new Dog();
console.log(dog5.name); // 小红
在上面我给Dog的prototype 对象 添加了 name age color bark 等属性
上面的原型模式与 原型链跟继承相关, 可以到这个链接去学习: 原型链与继承
既然我们可以获取到原型对象(prototype)的属性, 那如何区分是构造函数自己的属性还是 构造函数的原型对象中的属性
hasOwnProperty(key): 对象本身是否存在 key
console.log(dog5.hasOwnProperty('name')); // false
dog5.name = 'xiaohong';
console.log(dog5.hasOwnProperty('name')); // true
delete dog5.name;
console.log(dog5.name); // 小红
console.log(dog5.hasOwnProperty('name')); // true
在调用对象某个属性时,首先对象会检查自己本身是否有这个属性,如果不存在则会往上面的原型对象中找, 知道最顶层的原型对象中没有找到,才返回false。(还是需要学习上面的链接)
in 操作符:检查对象中是否存在这个属性, 无论是自身还是原型对象中
console.log('name' in dog5); // true
所以如果我们想知道某个对象的原型对象中是否存在这个属性可以这样写:
function hasPrototypeProperty(obj, key) {
return !obj.hasOwnProperty(key) && (key in obj);
}
原型模式也不是没有问题的:
function Person() {
}
Person.prototype.name = '小明';
Person.prototype.age = 14;
Person.prototype.friend = ['小红', '小强'];
var person1 = new Person();
var person2 = new Person();
console.log(person1.friend);// [ '小红', '小强' ]
console.log(person2.friend);// [ '小红', '小强' ]
person1.friend.push('小刚');
console.log(person1.friend);// [ '小红', '小强', '小刚' ]
console.log(person2.friend);// [ '小红', '小强', '小刚' ]
整个原型对象都是被实例共享的,假如我们不覆盖其对象进行操作,而是直接修改其属性,那最后就会向上面那样,导致所有实例获取到的数据都变成那样,虽然这样有其特殊用途,但我还是不太建议这样子使用。
组合使用构造函数模式和原型模式
function Person(name, age, friend) {
this.name = name;
this.age = age;
this.friend = friend;
}
Person.prototype.speak = function () {
console.log('你好,我是' + this.name)
}
var person1 = new Person('小明', 12, ['小红', '小强']);
var person2 = new Person('小白', 14, ['小红', '小强']);
console.log(person1.friend);// [ '小红', '小强' ]
console.log(person2.friend);// [ '小红', '小强' ]
person1.friend.push('小刚');
console.log(person1.friend);// [ '小红', '小强', '小刚' ]
console.log(person2.friend);// [ '小红', '小强' ]
person1.speak(); // 你好,我是小明
person2.speak(); // 你好,我是小白
动态原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.speak != 'function') {
Person.prototype.speak = function () {
console.log('你好,我是' + this.name);
}
}
}
这里的speak 只有在Person 不存在 该方法时才会调用
寄生构造函数模式: 当前面的几种的模式不适用的情况
function Person(name, age, friend) {
var o = {};
o.name = name;
o.age = age;
o.friend = friend;
return o;
}
var person1 = new Person('yang', 22, []);
除了使用new 方法以外, 其实这种方法跟工厂模式是一抹一样的。
构造函数在不返回值的情况下默认返回新对象实例,而在末尾添加一个return 可以重写构造函数的返回对象。
但是这种方式又有一个问题,这个返回的对象与构造函数之间其实是没有半毛钱关系的,也就是说如果你使用了 instanceof,它会返回false给你。由于存在这个问题,在其他模式可以使用的情况下,能不用就不用。
稳妥构造函数模式:前面几种模式当中都没有对对象的变量进行保护,所以就会有以下的写法,我们将在构造函数中定义一个返回对象,并且将参数赋予给构造函数中声明的变量,这样在外部始终是无法访问到 name age 和friend的,从而保证安全性。
// 稳妥构造函数模式
function Person(name, age, friend) {
var o = {};
// 以下定义了私有变量(安全)
var name = name;
var age = age;
var friend = friend;
o.speak = function () {
console.log('你好,我是' + name);
}
return o;
}
返回的对象就是一个稳妥对象,我们只能通过 o.speak() 没有其他办法来访问name。
借用构造函数:当原型包含引用类型值所带来问题的时候(意思是虽然我不能将你的变量拿过来用,但我可以通过其他方式来借用你的变量)
// 借用构造函数
function Richman() {
this.money = 10000000000;
this.lover = ['小丽', '小红', '千岛'];
}
function Poorman() {
// 虽然我一无所有,但是我能把你的钱复制过来
Richman.call(this);
}
var man1 = new Richman(); // 富人
var man2 = new Poorman(); // 穷人
console.log(man1); // 10000000000
console.log(man2); // 10000000000
man1.lover.pop(); // 富人的情人都跑光了
man1.money = 0; // 富人的钱没了
console.log(man1); // 一无所有
console.log(man2); // 家财万贯
这段代码没有任何隐义看看就行,就是利用call 或者apply 用Poorman当前环境执行了Richman的代码,让Poorman也有money 也有lover
组合继承(伪经典继承):
其中将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。既通过了原型链实现对原型属性和方法继承,又通过借用构造函数实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性
// 组合继承
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.speak = function () {
console.log(this.name);
};
function Type(name, age) {
SuperType.call(this, name);
this.age = age;
}
Type.prototype = new SuperType();
Type.prototype.constructor = Type;
Type.prototype.sayAge = function () {
console.log(this.age);
};
var type1 = new Type('小黄', 21);
type1.colors.push('black');
type1.sayAge();
type1.speak();
var type2 = new Type('小天', 22);
type2.colors.push('white');
type2.sayAge();
type2.speak();
console.log(type1.colors)
console.log(type2.colors)
原型式继承:
// 原型式继承
function object(o) {
function F() {
}
F.prototype = o;
return new F();
}
var obj = {
name: 'xxx',
age: 2
}
var newObj1 = object(obj);
var newObj2 = Object.create(obj);
首先要先自己创建一个对象,并以这个对象为基础实现创建另一个对象,这种模式与 Object.create(obj)行为一样。
你可以直接使用它来实现原型式继承。
------------------------------------------------------------------------------------------
下周更新 寄生式继承与 寄生组合式继承(这两个是面向对象很重要的两个概念,我会好好记录下来的)