JavaScript有两种开发模式:1.函数化(过程化),2.面向对象(OOP)。面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript没有类(class)的概念,因此,它的对象也与基于类的语言中的对象有所不同。
一、 对象
现在我想创建两个对象,分别是小强和小明,我们可以使用var obj = new Object
这种方式,也可以使用以下方式。
const xiaoming = {
name: 'xiaoming',
age: 18,
sayHello: function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
};
const xiaoqiang = {
name: 'xiaoqiang',
age: 16,
sayHello: function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
};
console.log(xiaoming.sayHello());
// hello, my name is xiaoming, I am 18 years old.
console.log(xiaoqiang.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
这种方式创建出的两个对象,我们发现有很多相同之处,比如说他们都有name,age,都有一样的sayHello方法。可否精简一下写法呢?于是我在第二个对象处做了如下更改:
const xiaoqiang = xiaoming;
xiaoqiang.name = 'xiaoqiang';
xiaoqiang.age = 16;
console.log(xiaoming.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
console.log(xiaoqiang.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
这里xiaoqiang和xiaoming在内存中的指针其实是一样的,指向同一个地方。修改其中一个,另外一个也会被修改。这是一个错误的演示。
1. 工厂模式
为了解决多个类似对象声明的问题,我们可以使用一种叫做工厂模式的方法,这种方法就是为了解决实例化对象产生大量重复的问题。其实就是写了一个函数,函数返回一个对象。这样就实现了传参而创造新的对象的想法。
// 工厂模式
function People(name, age) {
var obj = {
name,
age,
sayHello() {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
};
return obj;
}
const xiaoming = People('xiaoming', 18);
const xiaoqiang = People('xiaoqiang', 16);
console.log(xiaoming.sayHello());
// hello, my name is xiaoming, I am 18 years old.
console.log(xiaoqiang.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题,因为根本无法搞清楚他们是哪个对象的实例。instanceof运算符,验证原型对象与实例对象之间的关系。
console.log(typeof xiaoming); // Object
console.log(xiaoming instanceof Object);// true
console.log(xiaoming instanceof People);
// false 无法严重实例xiaoming与原型对象People之间的关系
ECMAScript中可以采用构造函数(构造方法)可用来创建特定的对象。类型于Object对象。
2. 构造函数
- 构造函数没有
new Object
,创建对象的写法,但是后台会自动创建对象; - this指向了创建实例的空对象;
- 构造函数不需要返回obj(对象引用)。后台自动返回;
function People(name, age) {
this.name = name;
this.age = age;
this.sayHello = function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
};
}
const xiaoming = new People('xiaoming', 18);
const xiaoqiang = new People('xiaoqiang', 16);
console.log(xiaoming.sayHello());
// hello, my name is xiaoming, I am 18 years old.
console.log(xiaoqiang.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
使用构造函数方法,即解决了重复实例化的问题,又解决了对象识别问题。
console.log(typeof xiaoming); // Object
console.log(xiaoming instanceof Object) // true
console.log(xiaoming instanceof People)
// true 可以验证xiaoming这个实例 就是从原型对象People而来
3. 构造函数的一些规范
- 构造函数也是函数,但是函数名第一个字母大写
- 创建实例必须使用new运算符,如new Obj(),而且这里第一个字母也是大写
function People(name, age) {
this.name = name; // 实例属性
this.age = age; // 实例属性
this.sayHello = function () { // 实例方法
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
};
}
const xiaoming = new People('xiaoming', 18); // 创建实例
const xiaoqiang = new People('xiaoqiang', 16); // 创建实例
构造函数和普通函数的唯一区别,就是他们的调用方式不一样。只不过,构造函数也是函数,必须用new运算符来调用,否则就是普通函数。
const xiaoming = new People('xiaoming', 18); // 构造模式调用
console.log(xiaoming.sayHello());
// hello, my name is xiaoming, I am 18 years old.
People('xiaoqiang', 18); // 普通模式调用无效
var xiaoqiang = {};
People.call(xiaoqiang, 'xiaoqiang', 16);
console.log(xiaoqiang.sayHello());
// hello, my name is xiaoqiang, I am 16 years old.
const people1 = new People('xiaoming', 18);
const people2 = new People('xiaoming', 18);
console.log(people1.name === people2.name);
// true
console.log(people1.sayHello === people2.sayHello);
// false 实例后的方法是两个不同指针的引用类型对象 引用类型绝对不相等
引用地址不一致,这样造成了一些资源上的浪费。因为两者的属性方法是一致的。那么如何实现让他们的引用地址一致呢。第一个想到的是,把这个函数写在外面,如下:
function People(name, age, sayHello) {
this.name = name;
this.age = age;
this.sayHello = sayHello;
}
function sayHello() {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
const people1 = new People('xiaoming', 18);
const people2 = new People('xiaoming', 18);
console.log(people1.name === people2.name);
// true
console.log(people1.sayHello === people2.sayHello);
// true 通过全局实现引用地址的一致 但是不推荐这样写,会出现恶意调用,而且代码阅读起来不舒服
// 这样我们就用到了原型这个概念,也就是prototype
二、 原型
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype是通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
首先,我们需要牢记两点:①__proto__
和constructor
属性是对象所独有的;② prototype
属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__
和constructor
属性,这点是致使我们产生困惑的很大原因之一。
- 每个对象都具有一个名为_proto_的属性;
- 每个构造函数(构造函数标准为大写开头,如Function(),Object()等等JS中自带的构造函数,以及自己创建的)都具有一个名为prototype的方法(注意:既然是方法,那么就是一个对象(JS中函数同样是对象),所以prototype同样带有_proto_属性);
- 每个对象的_proto_属性指向自身构造函数的prototype;
需要注意的指向是
Function的_proto_指向其构造函数Function的prototype;
Object作为一个构造函数(是一个函数对象!!函数对象!!),所以他的proto指向Function.prototype;
Function.prototype的_proto_指向其构造函数Object的prototype;
Object.prototype的_prototype_指向null(尽头);
下面这张图如果不要觉得麻烦,仔细阅读会被原型、原型链、构造器有些认识:
function People() {}
People.prototype.name = 'xiaoming';
People.prototype.age = 18;
People.prototype.sayHello = function sayHello() {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
};
const xiaoming = new People();
const xiaoqiang = new People();
console.log(xiaoming.sayHello());
// hello, my name is xiaoming, I am 18 years old.
console.log(xiaoming.sayHello === xiaoqiang.sayHello);
// true
//如果是实例方法,不同的实例化,他们的方法地址是不一样的,是唯一的。
//如果是原型方法,那么他们的地址是共享的,大家都是一样的。
构造函数方式:
构造函数方式创建的实例内部地址引用其实都不一样,只是不好测试,但是hello这个方法方便测试,因为是引用类型。
原型模式方式:
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。_proto_属性是实例指向原型对象(People.prototype)的一个指针通过这两个属性,就可以访问到原型里的属性和方法了。
console.log(xiaoming.__proto__);
// {name: "xiaoming", age: 18, sayHello: ƒ, constructor: ƒ}
console.log(xiaoming.__proto__ === People.prototype);
// true 实例的proto属性就是指向构造函数的prototype
console.log(xiaoming.constructor);
// ƒ People() {}
// 构造属性,可以获取函数本身
// 作用是被原型指针定位,然后得到构造函数本身
// 作用就是对象实例对应原型对象
console.log(People.constructor);
// 构造函数的构造器指向Function
判断一个对象实例(对象引用)是不是指向了对象的原型对象,实例化自动指向。
console.log(People.prototype.isPrototypeOf(xiaoming)); //true
原型模式的执行流程:
- 先查找构造函数实例里的属性或方法,如果有,立刻返回;
- 如果构造函数实例没有,则取它的原型对象里查找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
function People() {}
People.prototype.name = 'xiaoming';
People.prototype.age = 18;
People.prototype.sayHello = function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
};
var xiaoming = new People;
console.log(xiaoming.name);
// xiaoming 原型中的值 实例没有 访问原型
xiaoming.name = 'xiaoqiang';
console.log(xiaoming.name);
// xiaoqiang 就近原则
删除实例中的属性
delete xiaoming.name;
console.log(xiaoming.name);
// xiaoming 已经被删除
删除和覆盖原型中的属性
delete xiaoming.name;
delete People.prototype.name;
console.log(xiaoming.name);
// undefined 原型属性被删除
People.prototype.name = 'kakaxi';
console.log(xiaoming.name);
// kakaxi 覆盖原型属性
如何判断属性是在构造函数的实例里,还是在原型里?可以使用hasOwnProperty()函数来验证:
console.log(xiaoming.hasOwnProperty('name'));
// 判断实例中是否有属性 有true 否则false
console.log('name' in xiaoming);
// 不管实例属性或原型属性,只要有该属性就返回true 两边都没有返回false
判断只有原型中有属性
function isProperty(object, property) {
return !object.hasOwnProperty(property) && (property in object);
}
console.log(isProperty(xiaoming, 'name')); //true
为了让属性和方法更好的提现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
function People() {}
People.prototype = {
name: 'xiaoming',
age: 18,
sayHello: function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
};
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同。但还有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。
console.log(xiaoming.constructor);
//ƒ Object() { [native code] }
如果想让字面量的方式的constructor指向实例对象,那么可以这么做。
People.prototype = {
constructor: People //强制修改指向
};
字面量方式为什么constructor会指向Object?因为People.prototype={}这种写法实际上是创建了一个对象。而每创建一个对象,就会同时创建它的prototype,这个对象也会自动获得constructor属性。所以,新对象的constructor重写了People原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么默认为Object。
原型的声明是有先后顺序的,所以,重写原型会切断之前的原型。
People.prototype = {
age: 20
};
var xiaoming = new People();
console.log(xiaoming.name); //undefined
//这里等于修改了对象指向,重写了对象,没有之前数据。
内置引用类型
console.log(Array.prototype.splice);
// ƒ splice() { [native code] }
console.log(String.prototype.split);
// ƒ split() { [native code] }
//内置引用类型拓展,但是不利于维护,一般不推荐使用
String.prototype.addString = function () {
return this + ' ' + 'hello~~';
};
console.log('xiaoming'.addString());
// xiaoming hello~~
原型模式创建对象的缺点
它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值是一样的。而原型最大的缺点就是他的优点,那就是共享。
原型中所有的属性是被很多实例共享的(不能传参)。共享对于函数非常合适,对于包含基本值得属性也还可以、但是对于属性包含引用类型,就存在一些问题。
function People() {}
People.prototype = {
constructor: People,
name: 'xiaoming',
age: 18,
friend: ['卡卡西', '鸣人', '佐助'],
sayHello: function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
};
var xiaoming = new People();
xiaoming.friend.push('哈哈'); //在第一个实例修改后引用类型,保持了共享
console.log(xiaoming.friend);
["卡卡西", "鸣人", "佐助", "哈哈"]
var xiaoqiang = new People();
console.log(xiaoqiang.friend); //共享xiaoming添加后的引用类型
["卡卡西", "鸣人", "佐助", "哈哈"]
数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化的数据需要保留自己的特性,而不能更改。
解决这种问题可以组合构造函数+原型模式:
function People(name, age) {
this.name = name,
this.age = age,
this.friend = ['旗木卡卡西', '漩涡鸣人', '宇智波佐助']
}
People.prototype = {
constructor: People,
sayHello: function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
}
var xiaoming = new People('xiaoming', 18);
var libai = new People('libai', 20);
libai.friend.push('杜甫');
console.log(xiaoming.friend);
// ["旗木卡卡西", "漩涡鸣人", "宇智波佐助"]
console.log(libai.friend);
// ["旗木卡卡西", "漩涡鸣人", "宇智波佐助", "杜甫"]
这种混合模式很好解决了传参引用共享的问题,是创建对象比较好的办法。
原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又让人觉得很怪异。最好把构造函数和原型封装到一起。为了解决这个问题,我们可以使用动态原型模式。
动态原型模式
function People(name, age) {
this.name = name;
this.age = age;
this.friend = ['旗木卡卡西', '漩涡鸣人', '宇智波佐助'];
console.log('执行开始');
People.prototype.sayHello = function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
}
console.log('执行结束');
}
// 原型的初始化,只要第一次初始化就可以,没必要每次构造函数实例化的时候都初始化。
// 也就是new一个实例就初始化一次 以上console.log会分别执行实例个数次
var xiaoming = new People('xiaoming', 18);
var xiaoqiang = new People('xiaoqiang', 16);
解决方法:
if (typeof this.hello !== 'function') {
console.log('执行开始');
People.prototype.sayHello = function () {
return `hello, my name is ${this.name}, I am ${this.age} years old.`;
};
console.log('执行结束');
}
使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会切换实例于新原型之间的联系。
以上讲解了各种方式创建对象,如果这几种方式不能满足需求,还可以使用一开始的那种方式:寄生构造函数。
三、继承
继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而ECMAScript只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。
以下来自阮一峰博客,继承的五种方式。
简单说,现在有两个构造函数:
function Animal() {
this.species = '动物';
}
function Cat(name, color) {
this.name = name;
this.color = color;
}
如何让猫继承动物呢
一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function Cat(name, color) {
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
const xiaobai = new Cat('xiaobai', 'white');
console.log(xiaobai.species); // 动物
二、 prototype模式
第二种方法更常见,使用prototype属性。
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
每个函数都有一个prototype对象,前面说过。prototype对象中默认含有constructor对象,这个对象指向创建这个实例的构造函数。
// 通过原型链继承,超类实例化后的对象实例,复制给子类型的原型属性
Cat.prototype = new Animal();
// Animal {species: "动物"}
console.log(xiaobai.species); // 动物
但是这样绑定后Cat.prototype.constructor指向了Animal,这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
Cat.prototype.constructor = Cat;
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
三、 直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。
现在,我们先将Animal对象改写:
function Animal(){ }
Animal.prototype.species = "动物";
然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。
所以,上面这一段代码其实是有问题的。请看第二行
Cat.prototype.constructor = Cat;
这一句实际上把Animal.prototype对象的constructor属性也改掉了!
alert(Animal.prototype.constructor); // Cat
四、 利用空对象作为中介
由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
alert(Animal.prototype.constructor); // Animal
我们将上面的方法,封装成一个函数,便于使用。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用的时候,方法如下
extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
这个extend函数,就是YUI库如何实现继承的方法。
另外,说明一点,函数体最后一行
Child.uber = Parent.prototype;
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
五、 拷贝继承
上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。
首先,还是把Animal的所有不变属性,都放到它的prototype对象上。
function Animal(){}
Animal.prototype.species = "动物";
然后,再写一个函数,实现属性拷贝的目的。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:
extend2(Cat, Animal);
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
写到着 面向对象和原型继承有些晕。准备抽时间重新整理下。