JavaScript 面向对象浅析

数字筑梦
• 阅读 961

什么事类

类(class):类是对拥有同样属性(property)和行为的一系列对象(object)的抽象。 这里说的“行为”,在基于类的面向对象的语言中通常叫做类的方法(method)。而在 JavaScript 里,函数也是“一等公民”,可以被直接赋值给一个变量或一个对象的属性,因此在本文后续的讨论中,把“行为”也归入“属性”的范畴。

JavaScript 对类的实现

JavaScript 规定每一个对象都可以有一个原型([[prototype]] 内部属性)。(在实现 ECMAScript 5.1 规范以前,除了 Object.prototype 以外的对象都必须有一个原型。)每个对象都“共享”其原型的属性:在访问一个对象的属性时,如果该对象本身没有这个属性,则 JavaScript 会继续试图访问其原型的属性。这样,就可以通过指定一些对象的原型来使这些对象都拥有同样的属性。从而我们可以这样认为,在 JavaScript 中,以同一个对象为原型的对象就是属于同一个类的对象。

1. JavaScript 中对象的原型的指定方式

那么 JavaScript 中的对象与其原型是怎样被关联起来的呢?或者说,JavaScript 中的对象的原型是怎样被指定的呢?

1.1 new 操作符

JavaScript 有一个 new 操作符(operator),它基于一个函数来创建对象。这个用 new 操作符创建出来的对象的原型就是 new 操作符后面的函数(称为“构造函数”)的 prototype 属性。例如:

var obj = {"key": 1};
function fun() {}
fun.prototype = obj;
var a = new fun();

此时 fun 对象的原型就是 obj 对象。

1.2 Object.create 方法

Object.create 方法直接以给定的对象作为原型创建对象。一个代码例子:

var a = {"aa": 1};
var b = Object.create(a);

此时 b 对象的原型就是 a 对象。

1.3 Object.setPrototypeOf 方法

new 操作符和 Object.create 方法都是在创建一个对象的同时就指定其原型。而 Object.setPrototypeOf 方法则是指定一个已被创建的对象的原型。代码例子:

var a = {"aa": 1};
var b = Object.create(a);
// 此时 b 的原型是 a
var c = {"cc": 2};
Object.setPrototypeOf(b, c);
// 此时 b 的原型变为 c 了
1.4 隐式指定

数字、布尔值、字符串、数组和函数在 JavaScript 中也是对象,而它们的原型是被 JavaScript 隐式指定的:

  1. 数字(例如 1、1.1、NaN、Infinity)的原型是 Number.prototype;
  2. 布尔值(true 和 false)的原型为 Boolean.prototype;
  3. 字符串(例如 ""、"abc")的原型为 String.prototype;
  4. 函数(例如 function () {}、function (a) { return a + '1'; }) 的原型为 Function.prototype;
  5. 数组(如 []、[1, '2'])的原型是 Array.prototype;
  6. 用花括号直接定义的对象(如 {}, {"a": 1})的原型是 Object.prototype。
2 JavaScript 中定义类的代码示例

下面给出定义一个类的一段 JavaScript 代码的示例。它定义一个名为 Person 的类,它的构造函数接受一个字符串的名称,还一个方法 introduceSelf 会输出自己的名字。

// ----==== 类定义开始 ====----
function Person(name) {
    this.name = name;
}
Person.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
// ----==== 类定义结束 ====----
// 下面实例化一个 Person 类的对象
var someone = new Person("Tom");
// 此时 someone 的原型为 Person.prototype
someone.introduceSelf(); // 输出 My name is Tom

如果转换为 ECMAScript 6 引入的类声明(class declaration)语法,则上述 Person 类的定义等同于:

class Person {
    constructor(name) {
        this.name = name;
    }
    introduceSelf() {
        console.log("My name is " + this.name);
    }
}
3 对“构造函数”的再思考

在上面的例子中,假如我们不通过 Person.prototype 来定义 introduceSelf 方法,而是在构造函数中给对象指定一个 introduceSelf 属性:

function Person(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
var someone = new Person("Tom");
someone.introduceSelf(); // 也会输出 My name is Tom

虽然这种方法中,通过 Person 构造函数 new 出来的对象也都有 introduceSelf 属性,但这里 introduceSelf 变成了 someone 自身的一个属性而不是 Person 类的共有的属性:

function Person1(name) {
    this.name = name;
}
Person1.prototype.introduceSelf = function () {
    console.log("My name is " + this.name);
};
var a = new Person1("Tom");
var b = new Person1("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 输出 true
delete a.introduceSelf;
a.introduceSelf(); // 仍然会输出 My name is Tom,因为 introduceSelf 不是 a 自身的属性,不会被 delete 删除
b.introduceSelf = function () {
    console.log("I am a pig");
};
Person1.prototype.introduceSelf.call(b); // 输出 My name is Jerry
// 即使 b 的 introduceSelf 属性被覆盖,我们仍然可以通过 `Person1.prototype` 来让 b 执行 Person1 类规定的行为。
function Person2(name) {
    this.name = name;
    this.introduceSelf = function () {
        console.log("My name is " + this.name);
    };
}
a = new Person2("Tom");
b = new Person2("Jerry");
console.log(a.introduceSelf === b.introduceSelf); // 输出 false
// a 的 introduceSelf 属性与 b 的 introduceSelf 属性是不同的对象,分别占用不同的内存空间。
// 因此这种方法会造成内存空间的浪费。
delete a.introduceSelf;
a.introduceSelf(); // 会抛 TypeError
b.introduceSelf = function () {
    console.log("I am a pig");
};
// 此时 b 的行为已经与 Person2 类规定的脱节,对象 a 和对象 b 看起来已经不像是同一个类的对象了

但是这种方法也不是一无是处。例如我们需要利用闭包来实现对 name 属性的封装时:

function Person(name) {
    this.introduceSelf = function () {
        console.log("My name is " + name);
    };
}
var someone = new Person("Tom");
someone.name = "Jerry";
someone.introduceSelf(); // 输出 My name is Tom
// introduceSelf 实际用到的 name 属性已经被封装起来,在 Person 构造函数以外的地方无法访问
// name 相当于 Person 类的一个私有(private)成员属性

JavaScript 的类继承

类的继承实际上只需要实现:

  1. 子类的对象拥有父类定义的所有成员属性;
  2. 子类的任何一个构造函数都必须在开头调用父类的构造函数。

实现第 2 点的方式比较直观。而怎样实现第 1 点呢?其实我们只需要让子类的构造函数的 prototype 属性 (子类的实例对象的原型) 的原型是父类的构造函数的 prototype 属性 (父类的实例对象的原型),简而言之就是:把父类实例的原型作为子类实例的原型的原型。这样在访问子类的实例对象的属性时,JavaScript 会沿着原型链找到子类规定的成员属性,再找到父类规定的成员属性。而且子类可在子类构造函数的 prototype 属性中重载(override)父类的成员属性。

1 代码示例

下面给出一个代码示例,定义一个 ChinesePerson 类继承上文中定义的 Person 类:

function ChinesePerson(name) {
    Person.apply(this, name); // 调用父类的构造函数
}
ChinesePerson.prototype.greet = function (other) {
    console.log(other + "你好");
};
Object.setPrototypeOf(ChinesePerson.prototype, Person.prototype); // 将 Person.prototype 设为 ChinesePerson.prototype 的原型

var someone = new ChinesePerson("张三");
someone.introduceSelf(); // 输出“My name is 张三”
someone.greet("李四"); // 输出“李四你好”

上述定义 ChinesePerson 类的代码改用 ECMAScript 6 的类声明语法的话,就变成:

class ChinesePerson extends Person {
    constructor(name) {
        super(name);
    }

    greet(other) {
        console.log(other + "你好");
    }
}
2 重载父类成员属性的代码示例

你会不会觉得上面代码示例中,introduceSelf 输出半英文半中文挺别扭的?那我们让 ChinesePerson 类重载 introduceSelf 方法就好了:

ChinesePerson.prototype.introduceSelf = function () {
    console.log("我叫" + this.name);
};
var someone = new ChinesePerson("张三");
someone.introduceSelf(); // 输出“我叫张三”

var other = new Person("Ba Wang");
other.introduceSelf(); // 输出 My name is Ba Wang
// ChinesePerson 的重载并不会影响父类的实例对象
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
4年前
java中的类与对象(1)
    首先理解一下类和对象的关系,可以说:类是对象的抽象,对象是类的实例。类一个类中,通常上包含了属性和功能。属性通常用变量表达,功能通常上用函数表达。编写一个类class类名{//属性,用变量表达//功能,用函数表达}对象实例化对象的方法及含义:类名 对象名 new 
Wesley13 Wesley13
4年前
java面试考点解析(4):面向对象思想、系统架构设计
\ 一、面向对象思想 \1、面向对象的特征有哪些方面?答:面向对象的特征主要有以下几个方面:\抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象
Wesley13 Wesley13
4年前
java知识点二:面向对象
面向对象一.什么叫面向对象把一组数据结构和处理他们的方法组成对象(object),把相同行为的对象归纳为类(class),通过类的封装(encapsulation)隐藏内部细节,通过继承(inheritance)实现类的特化(specialization)/泛化(generalization),通过多态(polymorp
Wesley13 Wesley13
4年前
Java类和对象
一、类类是封装对象的属性和行为的载体,在Java语言中对象的属性以成员变量的形式存在,而对象的方法以成员方法的形式存在。1\.类的构造方法构造方法是一个与类同名的方法,对象的创建就是通过构造方法完成的,构造方法分为有参构造方法和无参构造方法,区别就在于有没有参数。说这么多概念是不是感觉有点麻木,直接看下面的例子吧。pub
Stella981 Stella981
4年前
JavaScript面向对象编程的15种设计模式
在程序设计中有很多实用的设计模式,而其中大部分语言的实现都是基于“类”。在JavaScript中并没有类这种概念,面向对象编程不是基于类,而是基于原型去面向对象编程,JS中的函数属于一等对象,而基于JS中闭包与弱类型等特性,在实现一些设计模式的方式上与众不同。ps:本文之讲述面向对象编程的设计模式策略,JavaScript原型的基础请参考阮一峰面向
Stella981 Stella981
4年前
JavaScript——面向对象以及基于类的继承
!(http://static.oschina.net/uploads/img/201601/15103654_Y7EU.gif)!(http://static.oschina.net/uploads/img/201601/15103654_PyU6.gif)ViewCode1/
Wesley13 Wesley13
4年前
Java面试参考指南(一)
Java面向对象相关概念Java是一种基于面向对象概念的编程语言,使用高度抽象化来解决现实世界的问题。    面向对象的方法将现实世界中的对象进行概念化,以便于在应用之间进行重用。例如:椅子、风扇、狗和电脑等。Java里的类(Class)是一个蓝图、模板,或者称之为原型,它定义了同一类事物的相同属性和行为。实例(Instan
Stella981 Stella981
4年前
Javascript定义类(class)的三种方法
在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法。Javascript语言不支持"类",但是可以用一些变通的方法,模拟出"类"。一、构造函数法这是经典方法,也是教科书必教的方法。它用构造函数模拟"类",在其内部用this关键字指代实例对象。  function
Wesley13 Wesley13
4年前
Java总论及三大特性理解
1、对象(object)   万物皆为对象(根类Object类)。   程序是对象的集合(面向对象程序设计语言OOP)。   每个对象都有自己的由其他对象所构成的存储(对象有成员属性)。   每个对象都拥有其类型(每个对象都是某个类class的一个实例instance)。某一特定类型的所有对象都可以接收
Stella981 Stella981
4年前
Javascript 面向对象编程
Javascript面向对象编程(一):封装Javascript是一种基于对象(objectbased)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。那么,如果我们要把"属性
Wesley13 Wesley13
4年前
Java基础语法入门
java基础语法一个Java程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。   对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。   类:类是一个模板,它描述一类对象的行为和状
数字筑梦
数字筑梦
Lv1
杨柳乍如丝,故园春尽时。
文章
5
粉丝
0
获赞
0