面向对象
背景
(1)对象是单个实物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2)对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal
对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
特征
-
封装
将一个事物所有的状态(属性),行为(方法)封装成一个对象
-
多态
封装的对象生成不同的单个对象
-
继承
直接创建对象
var obj = new Object();
//或
var obj = {};
//为对象添加方法,属性
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以这样
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
这种方式创建对象简单,但也存在一些问题:创建出来的对象无法实现对象的重复利用,并且没有一种固定的约束,操作起来可能会出现这样或者那样意想不到的问题。如下面这种情况。
var a = new Object();
var b = new Object();
var c = new Object();
c[a] = a;
c[b] = b;
console.log(c[a], a); //{} {}
console.log(c[a] === a); //输出什么 false
工厂模式
var createPerson = function (name, age) {
// 声明一个中间对象,该对象就是工厂模式的模子
var o = new Object();
// 依次添加我们需要的属性与方法
o.name = name;
o.age = age;
o.getName = function () {
return this.name;
};
return o;
};
// 创建两个实例
var perTom = createPerson("TOM", 20);
var PerJake = createPerson("Jake", 22);
console.log(perTom instanceof Object); //true
console.log(perTom instanceof createPerson); //false
console.log(perTom.__proto__, createPerson.prototype);//{} createPerson {} 实例的原型和构造函数的原型不一样
缺点:1.无法识别对象类型; 2.每个对象都有自己的 sayName 函数,函数不能共享,造成内存浪费
构造函数
const p1 = {
name: "foo",
};
function People(name) {
console.log(this); //{ name: 'foo' } People {}
this.name = name;
console.log(this); //{ name: 1 } People { name: 'boo' }
}
const Foo = People.bind(p1); //改变this指向,将Foo作为构造函数
Foo(1); //更改绑定的p1.name
console.log(p1); //{ name: 1 }
const foo = new Foo("boo");
console.log(foo.name); // boo
console.log(p1); //{ name: 1 }
构造函数模式和工厂模式存在一下不同之处:
- 没有显示的创建对象(new Object() 或者 var a = {})
- 直接将属性和方法赋给this对象
- 没有return语句
- 函数共享
原型链
①所有
引用类型
都有一个__proto__(隐式原型)
属性,属性值是一个普通的对象 ②所有函数
都有一个prototype(原型)
属性,属性值是一个普通的对象 ③所有引用类型的__proto__
属性指向构造函数的prototype
var a = [1,2,3];
console.log(a.__proto__ === Array.prototype;) // true
所有对象都有自己的原型对象(prototype)。原型对象的所有属性和方法,都能被实例对象共享。当我们访问对象的属性或者方法时,会优先访问实例对象自身的属性和方法。
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的
__proto__
隐式原型上查找,即它的构造函数的prototype
,如果还没有找到就会再在构造函数的prototype
的__proto__
中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链
。
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype
,即Object
构造函数的prototype
属性。也就是说,所有对象都继承了Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。
Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。
console.log(Object.getPrototypeOf(Object.prototype));// null
console.log(Object.prototype.__proto__ === null);
new 命令的机制
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
const Person = function (name, age) {
this.name = name;
this.age = age;
this.getName = function () {
return this.name;
}
}
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
const res = {};
if (func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.__proto__ = func.prototype;
}
console.log(arguments,);
// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向实例对象res
const ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象(即在构造函数中明确写了return this;)
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res;
}
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
const person1 = New(Person, 'tom', 20);//等同于New Person
console.log(person1.getName());
// 当然,这里也可以判断出实例的类型了
console.log(person1 instanceof Person); // true
使用new
命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给构造函数内部的
this
关键字。 - 开始执行构造函数内部的代码。
__proto__
当一个实例对象被创建时,这个构造函数将会把它的属性prototype赋给实例对象的内部属性__proto__
。proto是指向构造函数原型对象的指针。
constructor
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
由于constructor
属性定义在prototype
对象上面,意味着可以被所有实例对象继承。
function P() {}
var p = new P();
console.log(p.constructor === P); // true
console.log(p.constructor === P.prototype.constructor); // true
console.log(p.hasOwnProperty('constructor')); // false
console.log(P.prototype.hasOwnProperty('constructor'));
上面代码中,p
是构造函数P
的实例对象,但是p
自身没有constructor
属性,该属性其实是读取原型链上面的P.prototype.constructor
属性。
instanceof
instanceof 是用来判断 A 是否为 B 的实例(不能判断一个对象实例具体属于哪种类型)
表达式为:A instanceof B。如果 A 是 B 的实例,则返回 true,否则返回 false。
在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:
instanceof (A,B) = {
varL = A.__proto__;
varR = B.prototype;
if(L === R) {
// A的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:
[] instanceof Array; // true
{} instanceof Object;// true
newDate() instanceof Date;// true
function Person(){};
new Person() instanceof Person;
[] instanceof Object; // true
newDate() instanceof Object;// true
newPerson instanceof Object;// true
虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例
我们来分析一下 [ ]、Array、Object 三者之间的关系:
从 instanceof 能够判断出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最终 Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
从原型链可以看出,[] 的 proto 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
判断是否是数组
[] instanceof Array; // true
判断某个对象是否是某个构造函数的实例
function a(){}
let b = new a()
//判断实例的构造函数
console.log(b instanceof a) //true
继承
首先创建一个构造函数,并为其设置私有属性和公有属性。
// 定义一个人类
function Person(name) {
// 属性
this.name = name;
// 实例方法
this.sleep = function () {
console.log(this.name + "正在睡觉!");
};
}
// 原型方法
Person.prototype.eat = function (food) {
console.log(this.name + "正在吃:" + food);
};
原型链继承
重点圈起来:将父类实例赋值给子类原型对象
function Super(name, age) {
this.name = name;
this.age = age;
}
// 原型继承
Sub.prototype = new Super();
优点
简单易于实现,父类的新增的方法与属性子类都能访问。
缺点
1)可以在子类中增加实例属性,如果要新增加原型属性和方法需要在 new 父类构造函数的后面
2)创建子类实例时,不能向父类构造函数中传参数。
构造继承
重点圈起来:执行父构造,将This指向本身,拉取父私有属性
function Super(name, age, score) {
this.name = name;
this.age = age;
this.handle1 = () => {
console.log(this);
};
}
Super.prototype.score = 222;
Super.prototype.handle2 = () => {
console.log(this);
};
function Sub(name, age, sex, score) {
Super.call(this, name, age, score);
this.sex = sex;
}
const obj = new Sub(1, 2, 3);
console.log(obj.name, obj.score);//1 undefined
obj.handle1();//Sub { name: 1, age: 2, handle1: [λ], sex: 3 }
obj.handle2();//obj.handle2 is not a function
优点
只需要继承父类的属性时这种方式很简单。
缺点
只能继承父类自己的属性,父类原型上的属性与方法也不能继承。
组合继承
function Super(name, age, score) {
this.name = name;
this.age = age;
}
Super.prototype.score = 222;
function Sub(name, age, sex) {
Super.call(this, name, age);
this.sex = sex;
}
// 原型继承
Sub.prototype = new Super();
// 构造函数指向
// Sub.prototype.constructor = Sub;//需要赋值构造函数
const obj = new Sub(1, 2, 3, 4);
console.log(obj.name, obj.score);//1 222
console.log(obj.__proto__);//Super { name: undefined, age: undefined }
优点
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。而且,使用 instanceof 操作符和isPrototype()方法也能够用于识别基于组合继承创建的对象。
缺点
会调用两次父类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。