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 构造函数定义了两个属性:name 和 numLegs:

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

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

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

原型属性 Prototype Property

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

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

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

Bird.prototype.numLegs = 2;

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

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

由于所有的实例都可以继承 prototype 上的属性,所以可以把 prototype 看作是创建对象的 "配方"。 请注意:duck 和 canary 的 prototype 属于 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

在上一个挑战中创建的实例对象 duck 和 beagle 都有一个特殊的 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");

duck 从 Bird 构造函数那里继承了它的 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.prototype 的 prototype 就是 Object.prototype:

Object.prototype.isPrototypeOf(Bird.prototype);

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

继承 Inherit

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

function Animal() { };

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

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

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 就像是创建对象的“配方”。 如果把 animal 的 prototype 设置为与 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's 的 prototype中:

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

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

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

duck.eat() 将在控制台中显示字符串 nom nom nom, duck.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 在 duck 的 prototype 链上寻找方法的过程:

  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  阅读(60)  评论(0)    收藏  举报