首页 > 编程语言 >阅读周·你不知道的JavaScript | 行为委托,搞懂对象之间的关系

阅读周·你不知道的JavaScript | 行为委托,搞懂对象之间的关系

时间:2024-09-22 11:21:49浏览次数:12  
标签:Prototype .. 委托 对象 JavaScript Object 搞懂 prototype 属性

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。

《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。

已读完书籍《架构简洁之道》、《深入浅出的Node.js》

当前阅读周书籍《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》

行为委托

类的机制

阅读周·你不知道的JavaScript | 行为委托,搞懂对象之间的关系_构造函数

1、构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。

class CoolGuy {
  specialTrick = nothing;

  CoolGuy(trick) {
    specialTrick = trick;
  }

  showOff() {
    output("Here's my trick: ", specialTrick);
  }
}
Joe = new CoolGuy("jumping rope")

Joe.showOff() // 这是我的绝技:跳绳

上面的代码中,CoolGuy类有一个CoolGuy()构造函数,执行new CoolGuy()时实际上调用的就是它。构造函数会返回一个对象(也就是类的一个实例),之后可以在这个对象上调用showOff()方法,来输出指定CoolGuy的特长。

类的继承

在面向类的语言中,可以先定义一个类,然后定义一个继承前者的类。后者通常被称为“子类”,前者通常被称为“父类”。

1、多态

多态的一个方面是,任何方法都可以引用继承层次中高层的方法(无论高层的方法名和当前方法名是否相同)。

多态的另一个方面是,在继承链的不同层次中一个方法名可以被多次定义,当调用方法时会自动选择合适的定义。

上面的图1中,子类Bar应当可以通过相对多态引用(或者说super)来访问父类Foo中的行为。

2、多重继承

多重继承意味着所有父类的定义都会被复制到子类中。

多重继承的好处是可以把许多功能组合在一起。缺点是可能带来复杂问题,比如经典的钻石问题。

阅读周·你不知道的JavaScript | 行为委托,搞懂对象之间的关系_原型链_02

子类D继承自两个父类(B和C),这两个父类都继承自A。如果A中有drive()方法并且B和C都重写了这个方法(多态),那当D引用drive()时应当选择哪个版本呢(B:drive()还是C:drive())?

JavaScript要简单得多:它本身并不提供“多重继承”功能。

混入

在继承或者实例化时,JavaScript的对象机制并不会自动执行复制行为。JavaScript开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。

1、显式混入

开发者一般手动实现复制功能。这个功能在许多库和框架中被称为extend(..),但是为了方便理解我们称之为mixin(..)。

下面的代码实现一个非常简单的mixin(..)例子:

function mixin(sourceObj, targetObj) {
  for (var key in sourceObj) {
    // 只会在不存在的情况下复制
    if (!(key in targetObj)) {
      targetObj[key] = sourceObj[key];
    }
  }

  return targetObj;
}

var Vehicle = {
  engines: 1,
  ignition: function () {
    console.log('Turning on my engine.');
  },

  drive: function () {
    this.ignition();
    console.log('Steering and moving forward! ');
  },
};

var Car = mixin(Vehicle, {
  wheels: 4,
  drive: function () {
    Vehicle.drive.call(this);
    console.log('Rolling on all ' + this.wheels + ' wheels! ');
  },
});

Car中的属性ignition只是从Vehicle中复制过来的对于ignition()函数的引用。相反,属性engines就是直接从Vehicle中复制了值1。

2、隐式混入

var Something = {
  cool: function () {
    this.greeting = 'Hello World';
    this.count = this.count ? this.count + 1 : 1;
  },
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
  cool: function () {
    // 隐式把Something混入Another
    Something.cool.call(this);
  },
};

Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count不是共享状态)

通过在构造函数调用或者方法调用中使用Something.cool.call(this),实际上“借用”了函数Something.cool()并在Another的上下文中调用了它。最终的结果是Something.cool()中的赋值操作都会应用在Another对象上而不是Something对象上。

即把Something的行为“混入”到了Another中。

原型

[[Prototype]]

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。

var anotherObject = {
  a: 2,
};
// 创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);

for (var k in myObject) {
  console.log('found: ' + k);
}
// found: a

'a' in myObject; // true

通过各种语法进行属性查找时都会查找[[Prototype]]链,直到找到属性或者查找完整条原型链。

1、Object.prototype

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。

2、属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

myObject.foo = 'bar';

实际的过程是:

1)如果myObject对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值。

2)如果foo不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到foo, foo就会被直接添加到myObject上。

3)如果foo存在于原型链上层,赋值语句myObject.foo = "bar"的行为就会有些不同(而且可能很出人意料)。

如果属性名foo既出现在myObject中也出现在myObject的[[Prototype]]链上层,那么就会发生屏蔽。myObject中包含的foo属性会屏蔽原型链上层的所有foo属性,因为myObject.foo总是会选择原型链中最底层的foo属性。

(原型)继承

如果没有“继承”机制的话,JavaScript中的类就只是一个空架子。

阅读周·你不知道的JavaScript | 行为委托,搞懂对象之间的关系_方法名_03

这种图不仅展示出对象(实例)a1到Foo.prototype的委托关系,还展示出Bar.prototype到Foo.prototype的委托关系。

1、Object.create(..)

调用Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到指定的对象。

2、检查“类”关系

在传统的面向类环境中,检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)。

instanceof操作符只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系,但是不能判断两个对象(比如a和b)之间是否通过[[Prototype]]链关联。

function Foo() {
  // ...
}

Foo.prototype.blah = ...;

var a = new Foo();
a instanceof Foo; // true

Foo.prototype.isPrototypeOf(..)可以判断[[Prototype]]反射:

Foo.prototype.isPrototypeOf(a); // true

isPrototypeOf(..)回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype?

绝大多数(不是所有)浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:

a.__proto__ === Foo.prototype; // true

.__proto__实际上并不存在于对象中(本例中是a)。实际上,它和其他的常用函数一样,存在于内置的Object.prototype中。

对象关联

[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。

如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

1、创建关联

Object.create(..)可以创建想要的关联关系。

Object.create()的polyfill代码:

if (!Object.create) {
  Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
  };
}

上面这段polyfill代码使用了一个一次性函数F,通过改写它的.prototype属性使其指向想要关联的对象,然后再使用new F()来构造一个新对象进行关联。

行为委托

面向委托的设计

我们来尝试使用委托行为解决问题:

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 ... = ...

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

在上面的代码中,id和label数据成员都是直接存储在XYZ上(而不是Task)。通常来说,在[[Prototype]]委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标(Task)上。

在委托行为中则恰好相反:我们会尽量避免在[[Prototype]]链的不同级别中使用相同的命名,否则就需要使用笨拙并且脆弱的语法来消除引用歧义。

我们和XYZ进行交互时可以使用Task中的通用方法,因为XYZ委托了Task。

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

类与对象

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

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

从语法角度来说,我们同样没有使用任何构造函数、.prototype或new,实际上也没必要使用它们。

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


总结

我们来总结一下本篇的主要内容:

  • 类构造函数属于类,而且通常和类同名。此外,构造函数大多需要用new来调,这样语言引擎才知道你想要构造一个新的类实例。
  • 类意味着复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。
  • 如果要访问对象中并不存在的一个属性,[[Get]]操作就会查找对象内部[[Prototype]]关联的对象。这个关联关系实际上定义了一条“原型链”,在查找属性时会对它进行遍历。
  • 所有普通对象都有内置的Object.prototype,指向原型链的顶端(比如说全局作用域),如果在原型链中找不到指定的属性就会停止。toString()、valueOf()和其他一些通用的功能都存在于Object.prototype对象上,因此语言中所有的对象都可以使用它们。
  • 只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于[[Prototype]]的行为委托非常自然地实现。

作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞

标签:Prototype,..,委托,对象,JavaScript,Object,搞懂,prototype,属性
From: https://blog.51cto.com/u_15838863/12079842

相关文章

  • JavaScript(二)
    二、js语法2.5JSON2.5.1JSON简介JavaScriptObjectNotation,简称JSON。中文含义为“JavaScript对象表示法”。JSON是一种轻量级的数据交换格式,通常用于存储和网络传输。另外一个数据交换格式是XML。JSON的本质是字符串文本。是独立的语言,易于理解,因此被任何......
  • 如何使用 Javascript 确定二叉树是否相同
    介绍这里相同意味着结构和值都处于相同的位置。为了实现这一点,我们需要使用dfs算法,这样它也会检查深度。使用bfs算法无法实现这一点。所以这里我使用有序遍历来得到结果classNode{constructor(data){this.left=null;this.right=null;t......
  • 在 C# 和 JavaScript 之间选择进行网页抓取
    简单了解C#和JavaScript网页抓取的区别C#作为编译型语言,提供了丰富的库和框架,如HtmlAgilityPack、HttpClient等,方便实现复杂的网页爬取逻辑,并且代码简洁高效,具有较强的调试和错误处理能力能力。同时C#具有良好的跨平台支持,适用于多种操作系统。不过C#的学习曲线可能比较陡峭,需要一......
  • 在 JavaScript 中使用最小和最大堆管理流数据:数字运动员健康技术视角
    数据管理在健康技术中至关重要。无论是跟踪运动员的表现指标还是监控运动员的恢复时间,有效地组织数据都可以对洞察的获取方式产生重大影响。在这种情况下管理数据的一种强大工具是堆,特别是最小堆和最大堆。在这篇文章中,我们将使用与运动员数据管理相关的实际示例,探索如何在javasc......
  • 在 JavaScript 中掌握日期
    今天的重点是理解和操作javascript中的日期。日期是许多应用程序的一个基本方面,javascript提供了一组强大的工具来处理它们。1.理解javascript日期javascript日期是从1970年1月1日开始计算的,这被称为unix纪元。自那时起,它们通常以毫秒表示。以下是创建和查看......
  • 初学者 JavaScript
    JavaScript是一种高级编程语言,广泛应用于Web开发。它由BrendanEich于1995年创建,现已成为世界上最流行的编程语言之一。JavaScript主要用于前端Web开发,用于创建交互式用户界面和动态网页。它还可以在Node.js的帮助下用于后端Web开发,Node.js是一个运行时环境,允许开......
  • 利用 JavaScript 的集合和映射实现高效的内容管理系统
    javascript提供了几种强大的数据结构来处理数据集合。其中,map和set对于某些类型的任务特别有用。在本博客中,我们将探讨使用map和set解决常见编程问题的现实示例。理解地图和集合在深入示例之前,让我们快速回顾一下javascript中的map和set是什么。地图map是键值对......
  • 动态编程变得简单:带有 JavaScript 示例的初学者指南
    通过javascript中的动态编程释放高效解决问题的能力。介绍您想提高编程中解决问题的能力吗?动态规划(dp)是一种强大的技术,可以帮助您高效地解决复杂问题。本初学者指南将通过javascript示例向您介绍动态编程,使其易于掌握并应用于实际场景。您将学到什么:动态规划的基本概念......
  • 冒泡排序、选择排序、插入排序 - JavaScript 中的数据结构和算法
    排序算法是许多计算任务的支柱,在组织数据以实现高效访问和处理方面发挥着至关重要的作用。无论您是刚刚开始探索算法世界的初学者,还是希望刷新知识的经验丰富的开发人员,了解这些基本排序技术都是至关重要的。在这篇文章中,我们将探讨一些更基本的排序算法-冒泡排序、选择排序和插......
  • 【油猴脚本】00011 案例 Tampermonkey油猴脚本,动态渲染表格-实现页面动态-添加提取数
    前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦......