JavaScript 面向对象编程

面向对象编程 Object Oriented Programming

面向对象编程用对象把数据和方法聚合起来。

面向对象编程的优点

  • 能写出模块化的代码
  • 能使得代码更灵活
  • 能提高代码的可重用性

面向对象编程的原则

  • 继承(inheritance):子类/派生类从父类/基类/超类中派生,形成继承结构
  • 封装(encapsulation):代码的实现对用户不可见,例如调用 toUpperCase(),直接调用即可,不用考虑函数内部的实现
  • 抽象(abstraction):抽象就是抽象出你正在尝试做的事情的概念,而不是处理这个概念的具体表现形式
  • 多态(polymorphism):多种形态

对象

创建对象

  • 使用对象字面量创建:
let dog = {
    name: "Mit",
    numLegs: 2
}
  • 使用 Object.create() 创建:(所有的对象都来自某个类,对象是类的实例,也是一种层级结构)
class Animal {
	/* code */
}

var myDog = Object.create(Animal);
console.log(Animal);
  • 使用 new 关键字:(当使用默认或者空构造函数方法,JS 会隐式调用对象的超类来创建实例)
class Animal { /* code here */ }

var myDog = new Animal();
console.log(Animal)

访问对象属性

let dog = {
    name: "Mit",
    numLegs: 2
}
console.log(dog.name); // Mit

对象方法 Method

对象可以有一个叫做 method 的特殊属性。

方法属性也就是函数。 这给对象添加了不同的行为。

let dog = {
  name: "Spot",
  numLegs: 4,
  sayLegs: function() {
    return "This dog has 4 legs."
  }
};

dog.sayLegs();

this

this 代表上下文环境中引用的对象。

let dog = {
  name: "Spot",
  numLegs: 4,
  sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
};

dog.sayLegs(); // This dog has 4 legs.

构造函数

constructors 是创建对象的函数。 函数给这个新对象定义属性和行为。 可将它们视为创建的新对象的蓝图。

构造函数遵循一些惯例规则:

  • 构造函数函数名的首字母大写,这是为了方便区分构造函数( constructors)和其他非构造函数。
  • 构造函数使用 this 关键字来给它将创建的这个对象设置新的属性。 在构造函数里面,this 指向的就是它新创建的这个对象。
  • 构造函数定义了属性和行为就可创建对象,而不是像其他函数一样需要设置返回值。
function Dog() {
  this.name = "N";
  this.color = "red";
  this.numLegs = 2;
}

function Dog(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 4;
}

let terrier = new Dog("fsd","fa");

JS 中有很多内置对象类型,称为“原生对象(native object)”。有些类型可以使用构造函数获取一个实例,但是有些类型没有也不需要构造函数。例如 Date 可以用 new 关键字创建对象,但是 Math 对象就不行。

如果对象类型是基本类型(primitive),使用构造函数通常不是最佳实践:

let apple = new String('apple');
apple; // string

对于常规对象,最好使用对象字面量语法 {},而不是 new。例如:数组,函数,正则表达式等。

下列类型可以考虑构造函数:

new Date();
new Error();
new Map();
new Promise();
new Set();
new WeakSet();
new WeakMap();

使用构造函数创建对象

注意: 构造函数内的 this 总是指被创建的对象。

注意:通过构造函数创建对象的时候要使用 new 操作符。

function Dog() {
  this.name = "Rupert";
  this.color = "brown";
  this.numLegs = 4;
}
let hound = new Dog();

instanceof

凡是通过构造函数创建出的新对象,这个对象都叫做这个构造函数的 instance。 JavaScript 提供了一种很简便的方法来验证这个事实,那就是通过 instanceof 操作符。 instanceof 允许将对象与构造函数之间进行比较,根据对象是否由这个构造函数创建的返回 true 或者 false。 以下是一个示例:

let Bird = function(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 2;
}

let crow = new Bird("Alexis", "black");

crow instanceof Bird; // true

属性

自身属性 Own Property

请看下面的实例,Bird 构造函数定义了两个属性:namenumLegs

function Bird(name) {
  this.name  = name;
  this.numLegs = 2;
}

let duck = new Bird("Donald");
let canary = new Bird("Tweety");

namenumLegs 被叫做 自身属性,因为它们是直接在实例对象上定义的。 这就意味着 duckcanary 这两个对象分别拥有这些属性的独立副本。 事实上,Bird 的所有实例都将拥有这些属性的独立副本。

原型属性 Prototype Property

所有 Bird 实例可能会有相同的 numLegs 值,所以在每一个 Bird 的实例中本质上都有一个重复的变量 numLegs

当只有两个实例时可能并不是什么问题,但想象一下如果有数百万个实例。 这将会产生许许多多重复的变量。

更好的方法是使用 Birdprototypeprototype 是一个可以在所有 Bird 实例之间共享的对象。 以下是一个在 Bird prototype 中添加 numLegs 属性的示例:

Bird.prototype.numLegs = 2;

现在所有的 Bird 实例都拥有了共同的 numLegs 属性值。

console.log(duck.numLegs);
console.log(canary.numLegs);

由于所有的实例都可以继承 prototype 上的属性,所以可以把 prototype 看作是创建对象的 "配方"。 请注意:duckcanaryprototype 属于 Bird 的构造函数,即 Bird 的原型 Bird.prototype。 JavaScript 中几乎所有的对象都有一个 prototype 属性,这个属性是属于它所在的构造函数。

迭代属性 Iterate Property

自身属性是直接在对象上定义的。 而原型属性在 prototype 上定义。

function Dog(name) {
  this.name = name;
}

Dog.prototype.numLegs = 4;

let beagle = new Dog("Snoopy");

let ownProps = [];
let prototypeProps = [];

for (let property in beagle) {
  if (beagle.hasOwnProperty(property)) {
    ownProps.push(property); // let in 只能遍历自身属性
  } else {
    prototypeProps.push(property);
  }
}

构造函数属性 Constructor Property

在上一个挑战中创建的实例对象 duckbeagle 都有一个特殊的 constructor 属性:

let duck = new Bird();
let beagle = new Dog();

console.log(duck.constructor === Bird); // true
console.log(beagle.constructor === Dog); // true

需要注意到的是: constructor 属性是对创建实例的构造函数的一个引用。 constructor 属性的一个好处是可以通过检查这个属性来找出它是一个什么对象。
注意: 由于 constructor 属性可以被重写,所以最好使用instanceof 方法来检查对象的类型。

给对象添加属性

一种更有效的方法就是给对象的 prototype 设置为一个已经包含了属性的新对象。 这样一来,所有属性都可以一次性添加进来:

Bird.prototype = {
  numLegs: 2, 
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name);
  }
};

更改原型时,记得设置构造函数属性

手动设置一个新对象的原型有一个重要的副作用。 它清除了 constructor 属性! 此属性可以用来检查是哪个构造函数创建了实例,但由于该属性已被覆盖,它现在给出了错误的结果:

duck.constructor === Bird; // false
duck.constructor === Object; // true
duck instanceof Bird; // true

为了解决这个问题,凡是手动给新对象重新设置过原型对象的,都别忘记在原型对象中定义一个 constructor 属性:

Bird.prototype = {
  constructor: Bird,
  numLegs: 2,
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name); 
  }
};

原型 Prototype

在 JS 中,原型是一种属性可以被多个其他对象共享的对象。

对象的原型

来自同一原型的对象有相同的功能。

对象也可直接从创建它的构造函数那里继承其 prototype。 请看下面的例子:Bird 构造函数创建了一个 duck 对象:

function Bird(name) {
  this.name = name;
}

let duck = new Bird("Donald");

duckBird 构造函数那里继承了它的 prototype。 你可以使用 isPrototypeOf 方法来验证他们之间的关系:

Bird.prototype.isPrototypeOf(duck); // true

原型链 Prototype Chain

JavaScript 中所有的对象(除了少数例外)都有自己的 prototype。 而且,对象的 prototype 本身也是一个对象。

function Bird(name) {
  this.name = name;
}

typeof Bird.prototype; // object

正因为 prototype 是一个对象,所以 prototype 对象也有它自己的 prototype! 这样看来的话,Bird.prototypeprototype 就是 Object.prototype

Object.prototype.isPrototypeOf(Bird.prototype);

hasOwnProperty 是定义在 Object.prototype 上的一个方法,尽管在 Bird.prototypeduck上并没有定义该方法,但是依然可以在这两个对象上访问到。 这就是 prototype 链的一个例子。 在这个prototype 链中,Birdducksupertype,而 ducksubtypeObject 则是 Birdduck 实例共同的 supertypeObject 是 JavaScript 中所有对象的 supertype,也就是原型链的最顶层。 因此,所有对象都可以访问 hasOwnProperty 方法。

继承 Inherit

根据以上所说的 DRY 原则,通过创建一个 Animal supertype(或者父类)来重写这段代码:

function Animal() { };

Animal.prototype = {
  constructor: Animal, 
  describe: function() {
    console.log("My name is " + this.name);
  }
};

Animal 构造函数中定义了 describe 方法,可将 BirdDog 这两个构造函数的方法删除掉:

Bird.prototype = {
  constructor: Bird
};

Dog.prototype = {
  constructor: Dog
};

继承使用 extends 关键字,例如 class B extends class A

class Animal { /* code */ }
class Bird extends Animal { /* code */ }
class Eagle extends Bird { /* code */ }

从超类继承行为 Inherit Behaviors from a Supertype

这里用到构造函数的继承特性。 首先创建一个超类 supertype(或者叫父类)的实例。

let animal = new Animal();

此语法用于继承时会存在一些缺点。相反,另外一种没有这些缺点的方法来替代 new 操作:

let animal = Object.create(Animal.prototype);

Object.create(obj) 创建了一个新对象,并指定了 obj 作为新对象的 prototype。 回忆一下,之前说过 prototype 就像是创建对象的“配方”。 如果把 animalprototype 设置为与 Animal构造函数的 prototype 一样,那么就相当于让 animal 这个实例具有与 Animal 的其他实例相同的“配方”了。

第二个步骤:给子类型(或者子类)设置 prototype。 这样一来,Bird 就是 Animal 的一个实例了。

Bird.prototype = Object.create(Animal.prototype);

请记住,prototype 类似于创建对象的“配方”。 从某种意义上来说,Bird 对象的配方包含了 Animal 的所有关键“成分”。

let duck = new Bird("Donald");
duck.eat();

duck 继承了Animal 的所有属性,其中包括了 eat 方法。

super 关键字可以借用父类中的功能。

class Animal {
    constructor(color = 'yellow', energy = 100) {
        this.color = color;
        this.energy = energy;
    }
    isActive() {
        if(this.energy > 0) {
            this.energy -= 20;
            console.log('Energy is decreasing, currently at:', this.energy)
        } else if(this.energy == 0){
            this.sleep();
        }
    }
    sleep() {
        this.energy += 20;
        console.log('Energy is increasing, currently at:', this.energy)
    }
    getColor() {
        console.log(this.color)
    }
}

class Cat extends Animal {
    constructor(sound = 'purr', canJumpHigh = true, canClimbTrees = true, color, energy) {
        super(color, energy);
        this.sound = sound;
        this.canClimbTrees = canClimbTrees;
        this.canJumpHigh = canJumpHigh;
    }
    makeSound() {
        console.log(this.sound);
    }
}

class Bird extends Animal {
    constructor(sound = 'chirp', canFly = true, color, energy) {
        super(color, energy);
        this.sound = sound;
        this.canFly = canFly;
    }
    makeSound() {
        console.log(this.sound);
    }

注意⚠️:如果在子类中不使用 super 关键字,一旦运行上面的程序,会得到 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. 错误。

重置构造属性 Reset

当一个对象从另一个对象那里继承了其 prototype 时,那它也继承了父类的 constructor 属性。

请看下面的举例:

function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor // Bird

但是 duck 和其他所有 Bird 的实例都应该表明它们是由 Bird 创建的,而不是由 Animal 创建的。 为此,你可以手动将 Bird 的构造函数属性设置为 Bird 对象:

Bird.prototype.constructor = Bird;
duck.constructor // Bird

继承后添加方法

从超类构造函数继承其 prototype 对象的构造函数,除了继承的方法外,还可以拥有自己的方法。

除了从 Animal 构造函数继承的行为之外,还需要给 Bird 对象添加它独有的行为。 这里,给 Bird 对象添加一个 fly() 函数。 函数会以一种与其他构造函数相同的方式添加到 Bird'sprototype中:

Bird.prototype.fly = function() {
  console.log("I'm flying!");
};

现在 Bird 的实例中就有了 eat()fly() 这两个方法:

let duck = new Bird();
duck.eat();
duck.fly();

duck.eat() 将在控制台中显示字符串 nom nom nomduck.fly()将显示字符串 I'm flying!

重写继承的方法 Override

通过使用一个与需要重写的方法相同的方法名,向ChildObject.prototype 中添加方法。 请看下面的举例:Bird 重写了从 Animal 继承来的 eat() 方法:

function Animal() { }
Animal.prototype.eat = function() {
  return "nom nom nom";
};
function Bird() { }

Bird.prototype = Object.create(Animal.prototype);

Bird.prototype.eat = function() {
  return "peck peck peck";
};

如果一个实例:let duck = new Bird();,然后调用了 duck.eat(),以下就是 JavaScript 在 duckprototype 链上寻找方法的过程:

  1. duck => eat() 是定义在这里吗? 不是。
  2. Bird => eat() 是定义在这里吗? => 是的。 执行它并停止往上搜索。
  3. Animal => 这里也定义了 eat() 方法,但是 JavaScript 在到达这层原型链之前已停止了搜索。
  4. Object => JavaScript 在到达这层原型链之前也已经停止了搜索

多态 Polymorphism

多态来源于希腊语,意思是“多种形态”。

例如门和自行车上都有铃,但是它们的使用目的,形态都可能不同。

function ringTheBell(thing) {
  console.log(thing.bell())
}

ringTheBell(door);
ringTheBell(bicycle);

例如 concat() 方法和 + 都可以起到字符串拼接的作用,但是它们的形态不同。

多态性允许开发人员构建具有完全相同功能的对象,即具有完全相同名称、行为完全相同的函数。但同时,开发者可以在 OOP 结构的其他部分覆盖共享功能的某些部分,甚至覆盖完整功能。

举例:

class Bird {
  useWings() {
    console.log('flying');
  }
}

class Eagle {
  useWings() {
    super.useWings();
    console.log('barely flying');
  }
}

class Penguin extends Bird {
  useWings() {
    console.log('diving');
  }
}

var baldEagle = new Eagle();
var kingPenguin = new Penguin();
baldEagle.useWings(); // flying barely flying
kingPenguin.useWings() // diving

使用 Mixin 在不相关对象之间添加共同行为

对于不相关的对象,更好的方法是使用 mixins。 mixin 允许其他对象使用函数集合。

let bird = {
  name: "Donald",
  numLegs: 2
};

let boat = {
  name: "Warrior",
  type: "race-boat"
};

let glideMixin = function(obj) {
  obj.glide = function() {
    console.log("glide");
  }
};

glideMixin(bird);
glideMixin(boat);

私有化 Private

使属性私有化最简单的方法就是在构造函数中创建变量。 可以将该变量范围限定在构造函数中,而不是全局可用。 这样,属性只能由构造函数中的方法访问和更改。

在 JavaScript 中,函数总是可以访问创建它的上下文。 这就叫做 closure(闭包)。

function Bird() {
  let hatchedEgg = 10;

  this.getHatchedEggCount = function() { 
    return hatchedEgg;
  };
}
let ducky = new Bird();
ducky.getHatchedEggCount();

立即调用函数表达式(IIFE)

JavaScript 中的一个常见模式就是,函数在声明后立刻执行:

(function () {
  console.log("Chirp, chirp!");
})();

这是一个匿名函数表达式,立即执行并输出 Chirp, chirp!

请注意,函数没有名称,也不存储在变量中。 函数表达式末尾的两个括号()会让它被立即执行或调用。 这种模式被叫做立即调用函数表达式(immediately invoked function expression) 或者IIFE。

一个立即调用函数表达式(IIFE)通常用于将相关功能分组到单个对象或者是 module 中。 例如,先前的挑战中定义了两个 mixins:

function glideMixin(obj) {
  obj.glide = function() {
    console.log("Gliding on the water");
  };
}
function flyMixin(obj) {
  obj.fly = function() {
    console.log("Flying, wooosh!");
  };
}

可以将这些 mixins 分成以下模块:

let motionModule = (function () {
  return {
    glideMixin: function(obj) {
      obj.glide = function() {
        console.log("Gliding on the water");
      };
    },
    flyMixin: function(obj) {
      obj.fly = function() {
        console.log("Flying, wooosh!");
      };
    }
  }
})();

注意:一个立即调用函数表达式(IIFE)返回了一个 motionModule对象。 返回的这个对象包含了作为对象属性的所有 mixin 行为。 module 模式的优点是,所有的运动相关的行为都可以打包成一个对象,然后由代码的其他部分使用。 下面是一个使用它的例子:

motionModule.glideMixin(duck);
duck.glide();
posted @ 2023-05-11 09:08  Mitchell_C  阅读(15)  评论(0编辑  收藏  举报