读书笔记《你不知道的JavaScript(上卷)》——第二部分 this和对象原型

Posted onc-virn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读书笔记《你不知道的JavaScript(上卷)》——第二部分 this和对象原型相关的知识,希望对你有一定的参考价值。



第6章 行为委托

  • [[Prototype]]机制就是指对象中的一个内部链接引用另一个对象。
  • 如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
  • 换句话说,javascript中这个机制的本质就是对象之间的关联关系。

6.1 面向委托的设计

类 和 继 承 的 设 计 模 式 = > 委 托 行 为 的 设 计 模 式 类和继承的设计模式 => 委托行为的设计模式 =>

6.1.1 类理论

假设在软件中建模一些类似的任务(“XYZ”、“ABC”等)。

类设计方法:

  • 定义一个通用父(基)类,可以将其命名为Task,在Task类中定义所有任务都有的行为。
  • 接着定义子类XYZ和ABC,它们都继承自Task并且会添加一些特殊的行为来处理对应的任务。

非常重要的是,类设计模式鼓励在继承时使用方法重写和多态,比如说在XYZ任务中重写Task中定义的一些通用方法,甚至在添加新行为时通过super调用这个方法的原始版本。接下来会发现许多行为可以先“抽象”到父类然后再用子类进行特殊化(重写)。

伪代码:

class Task {
    id;

    // 构造函数Task()
    Task(ID) { id = ID; }
    outputTask() { output(id); }
}

class XYZ inherits Task {
    label;

    // 构造函数XYZ()
    XYZ(ID, Label) { super(ID); label = Label; }
    outputTask() { super(); output(label); }
}
class ABC inherits Task {
    // ...
}
  • 接下来可以实例化子类XYZ然后使用这些实例来执行任务“XYZ”。
  • 这些实例会复制Task定义的通用行为以及XYZ定义的特殊行为。
  • 同理,ABC类的实例也会复制Task的行为和ABC的行为。
  • 在构造完成后,通常只需要通过这些实例(而不是类)来完成任务,因为每个实例都有需要完成任务的所有方法和属性。

6.1.2 委托理论

委托设计方法:

  • 首先定义一个名为Task的对象(既不是类也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。
  • 接着,对于每个任务(“XYZ”、“ABC”)都会定义一个对象来存储对应的数据和行为。会把特定的任务对象都关联到Task功能对象上,让它们在需要的时候可以进行委托。
  • 基本上可以想象成,执行任务“XYZ”需要两个兄弟对象(XYZ和Task)协作完成。
  • 但是并不需要把这些行为放在一起,通过类的复制,可以把它们分别放在各自独立的对象中,需要时可以允许XYZ对象委托给Task。

伪代码:

Task = {
    setID: function(ID) { this.id = ID; },
    outputID: function() { console.log(this.id); }
};

// 让XYZ委托Task
XYZ = Object.create(Task);

XYZ.prepareTask = function(ID, Label) {
    this.setID(ID);
    this.label = Label;
};

XYZ.outputTaskDetails = function() {
    this.outputID();
    console.log(this.label);
};

// ABC = Object.create(Task);
// ABC ... = ...
  • 在这段代码中,TaskXYZ并不是类(或者函数),它们是对象。
  • XYZ通过Object.create(..)创建,它的[[Prototype]]委托了Task对象。

相比类利用子类重写父类方法达到的优势,委托相反,需要避免在[[Prototype]]链的不同级别中使用相同的命名

这个设计模式要求尽量少使用容易被重写的通用方法名,提倡使用更有描述性的方法名,尤其是要写清相应对象行为的类型。这样做实际上可以创建出更容易理解和维护的代码,因为方法名(不仅在定义的位置,而是贯穿整个代码)更加清晰(自文档)。

this.setID(ID); XYZ中的方法首先会寻找XYZ自身是否有setID(…),但是XYZ中并没有这个方法名,因此会通过[[Prototype]]委托关联到Task继续寻找,这时就可以找到setID(…)方法。此外,由于调用位置触发了this的隐式绑定规则,因此虽然setID(…)方法在Task中,运行时this仍然会绑定到XYZ,这正是想要的。

委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)

在API接口的设计中,委托最好在内部实现,不要直接暴露出去。在之前的例子中并没有让开发者通过API直接调用XYZ.setID()。(当然,可以这么做!)相反,把委托隐藏在了API的内部,XYZ.prepareTask(…)会委托Task.setID(…)

1.互相委托(禁止)

避免引用了一个两边都不存在的属性或者方法时,在[[Prototype]]链上产生一个无限递归的循环

2.调试

6.1.3 比较思维模型

面向对象风格:

function Foo(who) {
    this.me = who;
}
Foo.prototype.identify = function() {
    return "I am " + this.me;
};

function Bar(who) {
    Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);

Bar.prototype.speak = function() {
    console.log("Hello, " + this.identify() + ".");
};

let b1 = new Bar("b1");
let b2 = new Bar("b2");

b1.speak(); // Hello, I am b1.
b2.speak(); // Hello, I am b2.

子类Bar继承了父类Foo,然后生成了b1和b2两个实例。b1委托了Bar.prototype, Bar.prototype委托了Foo.prototype。

对象关联风格;

Foo = {
    init: function(who) {
      this.me = who;
    },
    identify: function() {
      return "I am " + this.me;
    }
};
Bar = Object.create(Foo);

Bar.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");

b1.speak();
b2.speak();
  • 这段代码中同样利用[[Prototype]]b1委托给Bar并把Bar委托给Foo
  • 非常重要的一点是,这段代码简洁了许多,只是把对象关联起来,并不需要那些既复杂又令人困惑的模仿类的行为(构造函数、原型以及new)。

类风格代码的思维模型强调实体以及实体间的关系:

简化版:

对象关联风格:

对象关联风格的代码显然更加简洁,因为这种代码只关注一件事:对象之间的关联关系。其他的“类”技巧都是非常复杂并且令人困惑的。去掉它们之后,事情会变得简单许多(同时保留所有功能)。

6.2 类与对象

Web开发中非常典型的一种前端场景:创建UI控件(按钮、下拉列表,等等):

6.2.1 控件“类”

一个包含所有通用控件行为的父类(可能叫作Widget)和继承父类的特殊控件子类(比如Button)。

在不使用任何“类”辅助库或者语法的情况下,使用纯JavaScript实现类风格的代码:

// 父类
function Widget(width, height) {
    this.width = width || 50;
    this.height = height || 50;
    this.$elem = null;
}

Widget.prototype.render = function($where){
    if (this.$elem) {
      this.$elem.css({
          width: this.width + "px",
          height: this.height + "px"
      }).appendTo($where);
    }
};

// 子类
function Button(width, height, label) {
    // 调用“super”构造函数
    Widget.call(this, width, height);
    this.label = label || "Default";
    this.$elem = $("<button>").text(this.label);
}

// 让Button“继承”Widget
Button.prototype = Object.create(Widget.prototype);

// 重写render(..)
Button.prototype.render = function($where) {
    // “super”调用
    Widget.prototype.render.call(this, $where);
    this.$elem.click(this.onClick.bind(this));
};

Button.prototype.onClick = function(evt) {
    console.log("Button '" + this.label + "' clicked! ");
};
$(document).ready(function(){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "Hello");
    var btn2 = new Button(150, 40, "World");

    btn1.render($body);
    btn2.render($body);
} );

ES6的class语法糖

class Widget {
    constructor(width, height) {
      this.width = width || 50;
      this.height = height || 50;
      this.$elem = null;
    }
    render($where){
      if (this.$elem) {
          this.$elem.css({
              width: this.width + "px",
              height: this.height + "px"
          }).appendTo($where);
      }
    }
}

class Button extends Widget {
    constructor(width, height, label) {
      super(width, height);
      this.label = label || "Default";
      this.$elem = $("<button>").text(this.label);
    }
    render($where) {
      super.render($where);
      this.$elem.click(this.onClick.bind(this));
    }
    onClick(evt) {
      console.log("Button '" + this.label + "' clicked! ");
    }
}
$(document).ready(function(){
    var $body = $(document.body);
    var btn1 = new Button(125, 30, "Hello");
    var btn2 = new Button(150, 40, "World");
    btn1.render($body);
    btn2.render($body);
} );

6.2.2 委托控件对象

同样的功能使用对象关联风格委托实现:

var Widget = {
    init: function(width, height){
      this.width = width || 50;
      this.height = height || 50;
      this.$elem = null;
    },
    insert: function($where){
      if (this.$elem) {
          this.$elem.css({
              width: this.width + "px",
              height: this.height + "px"
          }).appendTo($where);
      }
    }
};

var Button = Object.create(Widget);

Button.setup = function(width, height, label){
    // 委托调用
    this.init(width, height);
    this.label = label || "Default";

    this.$elem = $("<button>").text(this.label);
};
Button.build = function($where) {
      // 委托调用
      this.insert($where);
      this.$elem.click(this.onClick.bind(this));
  };
  Button.onClick = function(evt) {
      console.log("Button '" + this.label + "' clicked! ");
  };

  $(document).ready(function(){
      var $body = $(document.body);

      var btn1 = Object.create(Button);
      btn1.setup(125, 30, "Hello");

      var btn2 = Object.create(Button);
      btn2.setup(150, 40, "World");

      btn1.build($body);
      btn2.build($body);
  } );

使用对象关联风格来编写代码时不需要把WidgetButton当作父类和子类。相反,Widget只是一个对象,包含一组通用的函数,任何类型的控件都可以委托,Button同样只是一个对象。(当然,它会通过委托关联到Widget!)

从设计模式的角度来说,我们并没有像类一样在两个对象中都定义相同的方法名render(..),相反,我们定义了两个更具描述性的方法名(insert(..)build(..))。同理,初始化方法分别叫作init(..)setup(..)

在委托设计模式中,除了建议使用不相同并且更具描述性的方法名之外,还要通过对象关联避免丑陋的显式伪多态调用(Widget.callWidget.prototype.render.call),代之以简单的相对委托调用this.init(..)this.insert(..)

从语法角度来说,我们同样没有使用任何构造函数、.prototypenew,实际上也没必要使用它们。如果你仔细观察就会发现,之前的一次调用(var btn1 = new Button(..))现在变成了两次(var btn1 = Object.create(Button)和btn1.setup(..))。乍一看这似乎是一个缺点(需要更多代码)。

但是这一点其实也是对象关联风格代码相比传统原型风格代码有优势的地方。为什么呢?

使用类构造函数的话,你需要(并不是硬性要求,但是强烈建议)在同一个步骤中实现构造和初始化。然而,在许多情况下把这两步分开(就像对象关联代码一样)更灵活。

举例来说,假如你在程序启动时创建了一个实例池,然后一直等到实例被取出并使用时才执行特定的初始化过程。这个过程中两个函数调用是挨着的,但是完全可以根据需要让它们出现在不同的位置。

对象关联可以更好地支持关注分离(separation of concerns)原则,创建和初始化并不需要合并为一个步骤。

。。。这部分需要好好理解,直接照书书全搬过来了,后面仔细研读后再归纳。。。

6.3 更简洁的设计

有两个控制器对象,一个用来操作网页中的登录表单,另一个用来与服务器进行验证(通信)。

需要一个辅助函数来创建Ajax通信。它不仅可以处理Ajax并且会返回一个类Promise的结果,因此可以使用.then(…)来监听响应。

类设计模式中,会把基础的函数定义在名为Controller的类中,然后派生两个子类LoginController和AuthController,它们都继承自Controller并且重写了一些基础行为:

// 父类
function Controller() {
    this.errors = [];
}
Controller.prototype.showDialog = function(title, msg) {
    // 给用户显示标题和消息
};
Controller.prototype.success = function(msg) {
    this.showDialog("Success", msg);
};
Controller.prototype.failure = function(err) {
    this.errors.push(err);
    this.showDialog("Error", err);
};

// 子类
function LoginController() {
    Controller.call(this);
}
// 把子类关联到父类
LoginController.prototype = Object.create(Controller.prototype);
LoginController.prototype.getUser = function() {
	return document.getElementById("login username").value;
};
LoginController.prototype.getPassword = function() {
	return document.getElementById("login password

以上是关于读书笔记《你不知道的JavaScript(上卷)》——第二部分 this和对象原型的主要内容,如果未能解决你的问题,请参考以下文章

你不知道的javascript--上卷--读书笔记1

你不知道的javascript--上卷--读书笔记2

你不知道的JavaScript上卷 - 读书笔记 - 第2章词法作用域-2.2 欺骗词法

你不知道的JavaScript上卷 - 读书笔记 - 第2章词法作用域-2.2 欺骗词法

JavaScript中的this—你不知道的JavaScript上卷读书笔记

你不知道的Javascript(上卷)读书笔记之二 ---- 词法作用域