一、关于面向对象
编程可以分为面向过程编程和面向对象编程
面向过程:就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用
面向对象:把一件事分解成一个个对象,然后由对象之间分工合作。面向对象是以对象功能来划分问题,而不是步骤。
对象工厂
当需要创建具有相同结构的多个对象时,采用对象字面量或者new Object将很难适应,因此采用对象工厂模式来创建一个对象
{
// 很繁琐
let tom = {
name:"Tom",age:20,sex:"male"
}
let jerry = {
name:"Jerry",age:18,sex:"female"
}
}
{
// 对象工厂函数
function createPerson(name,age,sex){
return {name,age,sex}
};
let tom = createPerson("Tom",20,"male");
console.log(tom); // { name: 'Tom', age: 20, sex: 'male' }
let jerry = createPerson("Jerry",18,"female");
console.log(tom == jerry); // false
}
对象工厂也有问题,对象工厂本身是一个普通函数,用于表达对象结构时,描述性不强。对象工厂没有解决对象标识的问题,即创建的对象是什么类型,利用构造函数就可以解决对象工厂的问题
构造函数
构造函数的首字母要大写,与普通函数作为区分。
其工作原理:
- 在内存中创建一个新对象
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象(所以构造函数不需要return)
{
// 创建与使用
function Person(name,age){
this.name = name;
this.age = age;
this.show = function(){
console.log(`hello:${this.name}`);
}
}
let tom = new Person("tom",20);
let jack = new Person("Jack",22);
tom.show(); // hello:tom
}
构造函数用于创建特定类型的对象,如Object和Array等,以函数的形式为自己的对象类型定义属性和方法。
上面的例子中,tom和jack分别保存着Person的不同实例,这两个对象都有一个constructor属性指向Person。
{
console.log(tom.constructor == Person); // true
console.log(jack.constructor == Person); // true
}
constructor是用于标识对象类型的,不过一般使用instanceof操作符来确定对象的类型,所有自定义对象都继承自Object
{
console.log(tom instanceof Person); // true
console.log(tom instanceof Object); // true
}
构造函数的问题
构造函数虽然有用但也不是没有问题,主要问题在于其定义的方法会在每个实例上都创建一遍,因此对于刚刚的例子来说,每次实例化对象时都会创建show方法,但这个两个实例对象不是相等的,因此方法也不等,但是要做的事却是一样的,因此没必要定义两个不同的方法实例。解决这个问题可以通过把函数定义转移到构造函数外面。
{
function Person(name,age){
this.name = name;
this.age = age;
this.show = show
}
function show(){
console.log(`hello:${this.name}`)
}
let tom = new Person("tom",20);
let jack = new Person("Jack",22);
tom.show(); // hello:tom
// 不是相等的,因此显得冗余
console.log(tom.show == jack.show); // false
}
show函数被定义到了全局,在构造函数内部show属性等于全局的show函数,所以构造函数的实例对象共享了在全局上的show函数,这样虽然解决了重复的问题,但是全局作用域也被混乱了,因为show函数只能在实例对象上调用,如果构造函数有多个方法那么就要写多个全局函数,显得很麻烦,因此可以通过原型模式来解决。
原型模式
每一个函数都有一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上这个对象就是通过调用构造函数实例化的对象的原型。因此可以使用这个原型对象来定义属性或者方法,这样就可以被构造函数对象实例所共享。
{
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.grade = 'grade one';
// constructor属性指回构造函数
console.log(Person.prototype.constructor == Person); // true
}
{
// 构造函数的创建与使用
function Mouse(name,age){
this.name = name;
this.age = age;
}
Mouse.prototype.from = 'Cartoon'; // 都会被继承
let jerry = new Mouse('Jerry',19);
let mickey = new Mouse('Mickey',20)
// 当在构造函数原型上创建属性(或方法)时,会被改构造函数的额所有对象所共享。
console.log(jerry.from); // Cartoon
console.log(Object.getPrototypeOf(jerry) == Mouse.prototype); // true,同一个原型
console.log(Object.keys(jerry)); // keys返回可枚举的自己的属性。["name","age"]
console.log(Object.getOwnPropertyNames(jerry)); // 跟上面一样
console.log(jerry.hasOwnProperty("from")); // false,判断不了继承的属性
console.log('from' in jerry); // in判断可枚举的所有属性(包括继承属性)因此为true
// 一旦对象自身对继承自原型的属性赋值,则创建了一个属于自己的同名属性,并覆盖了继承自原型的属性。
mickey.from = 'Disney';
console.log("Jerry From:",jerry.from); // Jerry From: Cartoon
console.log("Mickey From:",mickey.from); // Mickey From: Disney
}
构造函数、原型与实例对象的关系
- 每个构造函数都有一个原型对象
- 原型有一个属性指回构造函数(constructor)
- 实例对象有一个内部指针[[Prototype]]指向原型
原型链
- 当对象原型是另一个构造函数的实例,如此迭代,形成了一连串的继承关系就是原型链
- 原型链表达了对象与对象之间的继承关系
原型链的问题
{
function Animal() {
this.colors = ["white", "black"];
}
function Mouse(name, age) {
this.name = name;
this.age = age;
}
// 强制指定原型对象,表达继承关系
Mouse.prototype = new Animal(); // 从Mouse的原型上继承animal构造函数的实例
let m1 = new Mouse("Mickey", 10);
console.log(m1.name, m1.colors);
m1.colors.push("red");
let m2 = new Mouse("Miney", 9);
console.log(m2.colors); // [ 'white', 'black', 'red' ],收到了影响
}
问题在于当原型中包含引用值时(如数组),在各实例间共享的是该引用值的引用。当某个实例修改该属性时会影响全部实例。并且子类在实例化时不能给父类传递参数
盗用构造函数:在子类构造函数中调用父类构造函数,并将子类当前实例指定为构造函数的上下文。
{
function Animal(type){
this.colors = ['white','black'];
this.type = type
}
function Mouse(name,age,type = 'Mouse'){
// 把父构造函数的this通过call改为当前构造函数的this
Animal.call(this,type); // 父类构造函数的盗用,解决传参的问题
this.name = name;
this.age = age;
}
Animal.prototype.show = function(){
console.log(this.type,this.colors);
}
// 强制指定原型对象,表达继承关系
Mouse.prototype = new Animal(); // 从Mouse的原型上继承animal构造函数的实例
Mouse.prototype.constructor = Mouse; // 强制指定构造函数为原构造函数
let m1 = new Mouse('Mickey',10);
m1.show(); // 就是Animal上面的show方法,this就为当前生成的实例对象的this Mouse [ 'white', 'black' ]
m1.colors.push('red');
console.log(m1.name,m1.type,m1.colors); // Mickey Mouse [ 'white', 'black', 'red' ]
let m2 = new Mouse('Duck',9);
m2.show();
m2.colors.push('pink');
console.log(m2.name,m2.type,m2.colors); // Duck Mouse [ 'white', 'black', 'pink' ]
console.log(m1 instanceof Mouse); // true
console.log(m1 instanceof Animal); // true
console.log(Object.keys(m1)); // [ 'colors', 'type', 'name', 'age' ]
console.log(Mouse.prototype.isPrototypeOf(m2)); // true
console.log(m1.constructor == Mouse); // true
console.log(m1.constructor == Animal); // false
}
二、ES6新增的class关键字
ES6中新引入了class关键字具有正式定义类的能力,class是一种新的语法糖可以显式的用来创建一个类,虽然表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念
{
// ES5定义类和继承
function Car(title){
this.title = title;
}
Car.prototype.drive = function(){
return 'Venoom';
}
const car = new Car("BMW");
console.log(car.title); // BMW
console.log(car.drive()); // Venoom
// ES5实现继承
function Benz(color,title){
Car.call(this,title); // 把当前对象作为this,改变父构造函数的this指向
this.color = color;
}
Benz.prototype = Object.create(Car.prototype);
Benz.prototype.constructor = Benz;
const ben = new Benz("red","focus");
console.log(ben.title); // focus
console.log(ben.drive()); // Venoom
}
{
// ES6定义类和继承
class People{
constructor(options){
this.name = options.name;
}
say(){
return 'hello';
}
}
// ES6的继承,使用extends关键字
class Boy extends People{
constructor(options){
super(options);
this.age = options.age;
}
}
const boy = new Boy({name:'小李',age:20})
console.log(boy.name); // 小李
console.log(boy.say()); // hello
}