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

onc-virn onc-virn     2022-12-14     612

关键词:



第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--上卷--读书笔记1

作用域是什么?  答:在《你不知道的javascript》书中提到,作用域就是根据名称查找变量的一套规则。古语有“无规矩不成方圆”,但是没有方圆,规矩又给谁用?所以个人理解作用域就是“规矩”+”方圆“。作用域是在创... 查看详情

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

...以访问它们被定义时所处的作用域中的任何变量,这就是JavaScript的闭包。闭包有哪些应用?  答:函数作为返回值:functionfoo(){vara=2;functionbar(){//bar拥有涵盖foo作用域的闭包,并对它保持 查看详情

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

this是什么?this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时... 查看详情

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

...要的工作类型,一种是词法作用域,一种是动态作用域,Javascript采用的是词法作用域,关于动态作用域的有兴趣的可以自行Google。1.词法阶段         首 查看详情

你不知道的javascript上卷-读书笔记-第2章词法作用域-2.2欺骗词法(代码片段)

你不知道的JavaScript上卷-读书笔记-第2章词法作用域-2.2欺骗词法作用域`eval`和`with`的缺点作用域JavaScript在ES6以前只有函数作用域。ES6开始支持块作用域:通常就是包裹的范围作用域中查找变量or函数的规则是从当前向... 查看详情

你不知道的javascript上卷-读书笔记-第2章词法作用域-2.2欺骗词法(代码片段)

你不知道的JavaScript上卷-读书笔记-第2章词法作用域-2.2欺骗词法作用域`eval`和`with`的缺点作用域JavaScript在ES6以前只有函数作用域。ES6开始支持块作用域:通常就是包裹的范围作用域中查找变量or函数的规则是从当前向... 查看详情

读书笔记你不知道的javascript(上卷)--作用域是什么

...1、理解作用域几个名词的介绍引擎:从头到尾负责整个JavaScript程序的编译及执行过程编译器:负责语法分析及代码生成器等脏活累活作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严... 查看详情

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

文章目录第6章行为委托6.1面向委托的设计6.1.1类理论6.1.2委托理论1.互相委托(禁止)2.调试6.1.3比较思维模型6.2类与对象6.2.1控件“类”ES6的class语法糖6.2.2委托控件对象6.3更简洁的设计反类6.4更好的语法反词法6... 查看详情

《你不知道的javascript上卷》学习笔记(代码片段)

第一部分:作用域和闭包一、作用域1.作用域:存储并查找变量的规则2.源代码在执行之前(编译)会经历三个步骤:分词/此法分析:将代码字符串分解成有意义的代码块(词法单元)解析/语法分析:将词法单元流转换成抽象语... 查看详情

你不知道的javascript中,读书笔记

七种内置类型null,undefined,boolean,number,string,object,symboltypeofnull===‘object‘//truenull是typeof是object的唯一的假值typeoffunction会返回‘function‘使用typeofx!==‘undefined‘比直接判断x更加安全,因为不会引发referenceerror 查看详情

你不知道的javascript(上卷)小结

上卷主要讲了作用域、闭包、this以及原型方面的内容。整体在github上瞥了一眼了原版的ydkjs,到目前修改篇幅有点大了,this和原型部分的目录已经不见了,应该是改动不少。说说本书的小缺点,一是,有些东西已经和实际不一... 查看详情

你不知道的javascript(上卷卷)笔记

<!DOCTYPEhtml><html><head><metacharset="utf-8"><title>你不知道的javascript(上卷)</title></head><body></body></html>   查看详情

你不知道的javascript(上卷)pdf

...sp; 内容简介  · · · · · ·JavaScript语言有很多复杂的概念,但却用简单的方式体现出来(比如回调函数),因此,JavaScript开发者无需理解语言内部的原理,就能编写出功能全面的程序;就像收音机... 查看详情

你不知道的javascript(上卷)

第一部分作用域和闭包第1章作用域是什么  1.1编译原理  1.2理解作用域    1.2.1演员表    1.2.2对话    1.2.3编译器有话说    1.2.4引擎和作用域的对话    1.2.5小测验  1.3作用域嵌套  1.4异常  1... 查看详情

读书笔记-你不知道的js中-promise

继续填坑模式  考虑下面的代码:functionfn(x){//dosomethingreturnnewPromise(function(resolve,reject){//调用resolve(..)和reject(...)});}varp=fn(2);  newPromise(..)模式通常称为revealingconstructor。传入函数会立即执行(不会像then(..)中的回调一样异步延 查看详情

读书笔记-你不知道的js上-对象

好想要对象···   函数的调用位置不同会造成this绑定对象不同。但是对象到底是什么,为什么要绑定他们呢?(可以可以,我也不太懂)  语法  对象声明有两个形式:  1、字面量=>varobj={...};  2、构造形式=&g... 查看详情

读书笔记-你不知道的js上-闭包与模块

闭包定义   当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。  看一段最简单的闭包代码:functionfoo(){vara=2;//闭包functionbar(){console.log(a);}returnbar;}//理论上foo执行完内部... 查看详情

《javascript设计模式》读书笔记二(封装和隐藏信息)

...你会看电视,可是你不知道电视的内部结构一样。可是在javascript中没有不论什么内置的机制。所以我们还 查看详情