JavaScript面向对象程序设计(上)


前言

面向对象( Object-Oriented, OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。前面提到过, ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。 ----《javascript高级程序设计(第3版)》

第 4 章中我们说过, JavaScript 和面向类的语言不同, 它并没有类来作为对象的抽象模式
或者说蓝图。 JavaScript 中只有对象。
实际上, JavaScript 才是真正应该被称为“面向对象” 的语言, 因为它是少有的可以不通
过类, 直接创建对象的语言。
在 JavaScript 中, 类无法描述对象的行为,(因为根本就不存在类! ) 对象直接定义自己的行为。 再说一遍, JavaScript 中只有对象。 ----《你不知道的JavaScript(上卷)》

从引用中我们可以看到,JavaScript与传统的OO语言不同,并没有真正的“面向类的程序设计”的概念,对象是JavaScript中十分重要的类型。
JavaScript为我们提供了“模仿类”的实现方案,以便于我们使用经典的OO语言的逻辑来设计和编写代码(也是现在大多数人使用的方案);但同时,我们在了解JavaScript的[[Prototype]]机制后,也可以回归语法本源,使用面向委托的逻辑进行设计。


一、JavaScript中对象的基本概念

JavaScript中有七种内置类型:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol

其中,object为引用类型,其余为基本类型,在JavaScript中数组(Array)、函数(Function)都属于object类型。

创建 Object 实例的方式有两种:

  1. 使用 new 操作符后跟 Object 构造函数:

    let person = new Object();
    person.name = "Bryce";
    person.age = 20;
    
  2. 对象字面量(对象字面量是对象定义的一种简写形式,目的在于简化创建
    包含大量属性的对象的过程):

    let person = {
      name : "Bryce",
      age : 20
    };
    

object类型下常用方法

  • Object.keys()
    遍历对象属性,传入的参数是一个对象,返回的是一个数组,数组中包含的是该对象所有的属性名
  • Object.values()
    返回一个数组,数组中为参数对象自身的(不含原型链上的)所有可遍历( enumerable )属性的键值(如果参数不是对象,Object.values会先将其转为对象。由于number和boolean的包装对象,都不会为实例添加非继承的属性,所以Object.values会返回空数组;而string会先转成一个类似数组的对象,字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组)
  • Object.create()
    创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  • Object.hasOwnProperty()
    判断对象自身(不含原型链上的)属性中是否具有指定的属性
  • Object.getOwnPropertyNames()
    返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性
  • Object.defineProperty()
    设置对象的数据属性或访问器属性
  • Object.assign()
    合并对象,将源对象( source )的所有可枚举属性,复制到目标对象( target ),如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性(可以用symbol类型做key解决这个问题)
  • Object.getPrototypeOf()
    返回指定对象的原型(内部[[Prototype]]属性的值)
  • Object.setPrototypeOf()
    设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null(性能不及Object.create())

理解Object.defineProperty()

object的属性:

  1. 数据属性
    数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性。
    [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为 true。
    [[Enumerable]]:表示能否通过 for-in 循环返回属性。默认值为 true。
    [[Writable]]:表示能否修改属性的值。默认值为 true。
    [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为 undefined。

    //使用Object.defineProperty()修改数据属性的例子
    var person = {};
    Object.defineProperty(person, "name", {
      writable: false,
      value: "Bryce"
    });
    alert(person.name); //"Bryce"
    person.name = "Greg";
    alert(person.name); //"Bryce"
    
  2. 访问器属性
    访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。
    [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。默认值为true。
    [[Enumerable]]:表示能否通过 for-in 循环返回属性。默认值为 true。
    [[Get]]:在读取属性时调用的函数。默认值为 undefined。
    [[Set]]:在写入属性时调用的函数。默认值为 undefined。

    //使用Object.defineProperty()修改访问器属性的例子
    var book = {
      _year: 2004,
      edition: 1
    };
    Object.defineProperty(book, "year", {
      get: function(){
        return this._year;
      },
      set: function(newValue){
        if (newValue > 2004) {
          this._year = newValue;
          this.edition += newValue - 2004;
        }
      }
    });
    book.year = 2005;
    alert(book.edition); //2
    

二、使用JS对象前要了解什么

1.“模仿类”带来的误导

有些语言(比如 Java) 并不会给你选择的机会, 类并不是可选的——万物皆是类。 其他语言(比如 C/C++ 或者 PHP) 会提供过程化和面向类这两种语法, 开发者可以选择其中一种风格或者混用两种风格。
JavaScript 属于哪一类呢? 在相当长的一段时间里, JavaScript 只有一些近似类的语法元素(比如 new 和 instanceof), 不过在后来的 ES6 中新增了一些元素, 比如 class 关键字。
这是不是意味着 JavaScript 中实际上有类呢? 简单来说: 不是。
由于类是一种设计模式, 所以你可以用一些方法(本章之后会介绍) 近似实现类的功能。为了满足对于类设计模式的最普遍需求, JavaScript 提供了一些近似类的语法。
虽然有近似类的语法, 但是 JavaScript 的机制似乎一直在阻止你使用类设计模式。 在近似类的表象之下, JavaScript 的机制其实和类完全不同。 语法糖和( 广泛使用的)JavaScript“类” 库试图掩盖这个现实, 但是你迟早会面对它: 其他语言中的类和 JavaScript中的“类” 并不一样。 ----《你不知道的JavaScript(上卷)》

传统的类被实例化时,他的行为会被复制到实例中;类被继承时,行为也会被复制到子类中;多态看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。

但是,JavaScript并不会像类那样自动创建对象的副本。

就算不使用面向委托的设计,在JavaScript “模仿类”的过程中,“实例化”是在新建的对象上调用“构造函数”,并创建关联;“继承”只是在两个对象之间创建一个关联,一个对象可以通过这层“关联”访问到另一个对象的方法和属性,这层“关联”就是原型链

然而,“类”、“继承”、“构造函数”、“实例”、“多态”等这样令人容易混淆的术语也经常出现在JavaScript中,但是我们要知道,这些经典的OO语言术语是为了方便我们理解JavaScript中类似的操作的设计思路,而并不代表实际的实现机制。

——本文也会多次出现这些术语,但请再次注意,他们在JavaScript中的实现机制和传统OO语言并不相同

2.new

new可以帮助我们在JavaScript中创建一个新“实例”,其实现原理如下:

//模拟一个new
function myNew(constrc, ...args) {
    const obj = {}; // 1. 创建一个空对象
    obj.__proto__ = constrc.prototype; // 2. 将obj的[[prototype]]属性指向构造函数的原型对象(进行了关联)
    constrc.apply(obj, args); // 3.将constrc执行的上下文this绑定到obj上,并调用
    return obj;  //4. 返回新创建的对象
}

// 使用的例子:
//创建“构造函数”Person
function Person(name, age){
    this.name = name;
    this.age = age;
}
const person1 = myNew(Person, 'Tom', 20);
console.log(person1);  // Person {name: "Tom", age: 20}

这里的“构造函数”并不是真正的构造函数,只是一个普通的函数,只是可以把通过new来进行调用的函数暂且称为“构造函数”(实现了类似其他OO语言中构造函数的功能)

3.原型链

让我们通过例子来了解下原型链的概念,以及顺便看看原型链是如何帮助我们实现对“类”的模拟的:

首先创建一个Object“实例”:

let obj = new Object();

在这里插入图片描述

用这张图片来理解其中的几个概念:

  • prototype属性:
    prototype属性是函数所独有的,从一个函数指向一个对象,含义是函数的原型对象;作用是创建可查找由特定类型的所有实例共享的属性和方法的关联,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,会默认同时创建该函数的prototype对象。
  • __proto__属性:
    __proto__属性是对象所独有的(函数作为对象也拥有该属性),__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象;它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象里找,如果还没找到,则继续往上找…直到原型链顶端null,这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链
  • constructor属性:
    constructor属性也是对象所独有的(函数作为对象也拥有该属性),但实际上constructor本身只是在某个函数被声明时产生的默认属性,对象(这里一般指xxx.prototype)的.constructor属性默认指向一个函数,而这个函数也有一个.prototype的引用指向这个对象,这种关联其实是松散的,因为constructor并不是一个不可变属性,它的值可以任意修改,而且当使用新创建的对象赋予xxx.prototype时,.constructor也会丢失,所以constructor是不可信赖的,虽然通过new()产生的对象可以访问.constructor属性,并在一般情况下访问到“构造”出该对象的函数,但是其实这个constructor属性并不是对象自己的,而是通过原型链[[Prototype]]的默认委托访问到的,不能把constructor的指向理解为“…由…构造”(因为关联很容易被破坏),且最好避免使用这个引用。Function这个对象比较特殊,它的构造函数就是它自己(因为Function可以看成是一个函数,也可以是一个对象),所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数

接下来来看一个我们经常会遇到的情况:

function Student() {/*这里是一个“构造函数”*/};
let student1 = new Student();

通过刚刚的理解,我们就能比较清楚地画出原型链图解了(为了不过于混乱,省略了上图中虚线框中的部分):
在这里插入图片描述

4.Object.create()

方法说明:

  1. Object.create()方法创建一个新的对象,并以方法的第一个参数作为新对象的__proto__属性的值(以第一个参数作为新对象的构造函数的原型对象)
  2. Object.create()方法还有第二个可选参数,传入一个对象,对象的每个属性都会作为创建出的新对象的自身属性,且enumerable默认为false
Object.myCreate = function (proto, propertyObject = undefined) {
  if (propertyObject === null) {
    throw 'TypeError';
  } else {
    function F (){}
    Fn.prototype = proto;
    const obj = new F();
    if (propertyObject !== undefined) {
      Object.defineProperties(obj, propertyObject);
    }
    if (proto === null) {
      // 创建一个没有原型对象的对象
      obj.__proto__ = null;
    }
    return obj;
  }
}

Object.create()会创建一个新对象并将其关联到我们指定的对象,这样我们就可以充分发挥原型链机制的威力(委托)并且避免使用new可能带来的一些麻烦,通过Object.create()创建出的关联关系是纯粹的关联关系(后续会进行使用)。

Object.create(null) 会 创 建 一 个 拥 有 空( 或 者 说 null) [[Prototype]]链接的对象, 这个对象无法进行委托。 由于这个对象没有原型链, 所以instanceof 操作符( 之前解释过) 无法进行判断, 因此总是会返回 false。这些特殊的空 [[Prototype]] 对象通常被称作“字典”, 它们完全不会受到原型链的干扰, 因此非常适合用来存储数据 ----《你不知道的JavaScript(上卷)》

总结

本文分析了JavaScript对象的一些基本概念理论和容易产生误解的地方,在我们理解了JS作为面向对象语言对模仿“类”设计模式的运用与支持,以及由于其独特的“原型链”结构所导致的与其他面向对象语言十分不同的实现方案后,我们才能更清楚地去分析JS中各种创建对象的模式,ES6中的Class语法以及实现“继承”的模式等。
在下半段文章中将继续分析上述内容,以及如何回归语法本源,使用面向委托的设计,在不借助“类”的设计模式下完成相同的工作。


后篇

JavaScript面向对象程序设计(下)


参考书籍:《JavaScript高级程序设计(第3版)》、《你不知道的JavaScript(上卷)》

版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者昵称,未经作者允许不得用于商业目的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值