前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Prototype (原型)

Prototype (原型)

作者头像
Cellinlab
发布2023-05-17 15:22:05
3210
发布2023-05-17 15:22:05
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 原型

# [[Prototype]]

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

代码语言:javascript
复制
var myObj = {
  a: 2
};
myObj.a; // 2

如果 a 不在 myObj 中,就需要使用对象的 [[Prototype]] 链了。对于默认的 [[Get]] 操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的 [[Prototype]] 链:

代码语言:javascript
复制
var anotherObj = {
  a: 2
};

var myObj = Object.create(anotherObj);
myObj.a; // 2

如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条 [[Prototype]] 链。如果是后者的话,[[Get]] 操作的返回值是 undefined

使用 for..in 遍历对象时原理和查找 [[Prototype]] 链类似,任何可以通过原型链访问到(并且是 enumerable)的属性都会被枚举。使用 in 操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举 ):

代码语言:javascript
复制
var anotherObj = {
  a: 2
};

var myObj = Object.create(anotherObj);

for (var k in myObj) {
  console.log(i);
}
// "a"
('a' in myObj); // true

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

# Object.prototype

所有普通[[Prototype]] 链最终都会指向内置的 Object.prototype 。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为)这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。

# 属性设置和屏蔽

代码语言:javascript
复制
myObj.foo = 'bar';

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

如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo ="bar" 会出现的三种情况:

  1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性 。
  2. 如果在 [[Prototype]] 链上层存在 foo ,但是它被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  3. 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter,那就一定会调用这个 setterfoo 不会被添加到(或者说屏蔽于)myObject ,也不会重新定义 foo 这个 setter

# “类”

JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式或者说蓝图。JavaScript 中只有 对象。

在 JavaScript 中,类无法描述对象的行为,(因为根本就不存在类!)对象直接定义自己的行为。

# “类” 函数

多年以来,JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿 类。“类似类”的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象:

代码语言:javascript
复制
function Foo () {
  // ...
}
Foo.prototype; // { constructor: Foo }

这个对象通常被称为 Foo 的原型 ,因为我们通过名为 Foo.prototype 的属性引用来访问它。

这个对象到底是什么?这个对象是在调用 new Foo() 时创建的,最后会被(有点武断地)关联到这个“Foo.prototype” 对象上。

代码语言:javascript
复制
function Foo() {
  // ...
}
var foo = new Foo();
Object.getPrototypeOf(foo) === Foo.prototype; // true

调用 new Foo() 时会创建 foo,其中的一步就是给 foo 一个内部的 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象。

在 JavaScript 中,没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。

new Foo() 会生成一个新对象(foo),这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象。

最后得到了两个对象,它们之间互相关联,就是这样。并没有初始化一个类,实际上并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。

实际上,绝大多数 JavaScript 开发者不知道的秘密是,new Foo() 这个函数调用实际上并没有直接 创建关联,这个关联只是一个意外的副作用。new Foo() 只是间接完成了我们的目标:一个关联到其他对象的新对象。

在 JavaScript 中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。从视觉角度来说,[[Prototype]] 机制如下图所示,箭头从右到左,从下到上:

这个机制通常被称为原型继承,它常常被视为动态语言版本的 类继承。

在“继承”前面加上“原型”对于事实的曲解就好像一只手拿橘子一只手拿苹果然后把苹果叫作“红橘子”一样。无论添加什么标签都无法改变事实 :一种水果是苹果,另一种是橘子。更好的方法是直接把苹果叫作苹果——使用更加准确并且直接的术语。

继承 意味着复制操作,JavaScript (默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托 访问另一个对象的属性和函数。委托 这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

# “构造函数”
代码语言:javascript
复制
function Foo() {
  // ...
}
var foo = new Foo();

到底是什么让我们认为 Foo 是一个“类”呢?其中一个原因是我们看到了关键字 new ,在面向类的语言中构造类实例时也会用到它。另一个原因是,看起来我们执行了类的构造函数方法,Foo() 的调用方式很像初始化类时类构造函数的调用方式。

除了令人迷惑的“构造函数”语义外,Foo.prototype 还有另一个绝招。

代码语言:javascript
复制
function Foo() {
  // ...
}
Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true

Foo.prototype 默认有一个公有并且不可枚举的属性 constructor ,这个属性引用的是对象关联的函数(本例中是 Foo )。此外,我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 constructor 属性,指向“创建这个对象的函数”。

  1. 构造函数还是调用

实际上,Foo 和程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。

代码语言:javascript
复制
function NothingSpecial () {
  console.log(`Don't mind me!`);
}

var a = new NothingSpecial(); // 输出 "Don't mind me!"

NothingSpecial 只是一个普通的函数,但是使用 new 调用时,它就会构造 一个对象并赋值给 a ,这看起来像是 new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数调用,但是 NothingSpecial 本身并不是一个构造函数。

换句话说,在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。

# (原型)继承

原型继承机制,a 可以“继承” Foo.prototype 并访问 Foo.prototypemyName() 函数。但是之前我们只把继承看作是类和类之间的关系,并没有把它看作是类和实例之间的关系:

上图,它不仅展示出对象(实例)a1Foo.prototype 的委托关系,还展示出 Bar.prototypeFoo.prototype 的委托关系,而后者和类继承很相似 ,只有箭头的方向不同。图中由下到上的箭头表明这是委托关联,不是复制操作。

代码语言:javascript
复制
function Foo(name) {
  this.name = name;
}
Foo.prototype.myName = function() {
  return this.name;
};

function Bar(name, label) {
  Foo.call(this, name);
  this.label = label;
}

// 创建一个新的 Bar.prototype 对象 关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);

Bar.prototype.myLabel = function() {
  return this.label;
};

var a = new Bar("a1", "obj a");
a.myName(); // "a1"
a.myLabel(); // "obj a"

声明 function Bar() { .. } 时,和其他函数一样,Bar 会有一个 .prototype 关联到默认的对象,但是这个 对象并不是我们想要的 Foo.prototype 。因此我们创建了一个新 对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。

两种方式是常见的错误做法,实际上它们都存在一些问题:

代码语言:javascript
复制
// 实现机制不一样
// Bar.prototype = Foo.prototype 并不会创建一个关联到Bar.prototype 的新对象
// 它只是让Bar.prototype 直接引用Foo.prototype 对象
// 因此当执行类似Bar.prototype.myLabel = ... 的赋值语句时会直接修改Foo.prototype 对象本身
Bar.prototype = Foo.prototype;

// 基本满足要求,但会有一些副作用
Bar.prototype = new Foo();

# 检查 “类” 关系
代码语言:javascript
复制
a instanceof Foo; // true

instanceof 回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?

这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo )之间的关系。如果你想判断两个对象 (比如 ab )之间是否通过 [[Prototype]] 链关联,只用 instanceof 无法实现。

代码语言:javascript
复制
Foo.prototype.isPrototypeOf(a); // true

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

同样的问题,同样的答案,但是在第二种方法中并不需要间接引用函数(Foo),它的 .prototype 属性会被自动访问。

也可以直接获取一个对象的 [[Prototype]] 链。在 ES5 中,标准的方法是:

代码语言:javascript
复制
Object.getPrototypeOf(a); // Foo.prototype

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

代码语言:javascript
复制
a.__proto__ === Foo.prototype; // true

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

.__proto__ 看起来很像一个属性,但是实际上它更像一个 getter/setter。

# 对象关联

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

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

# 创建关联
代码语言:javascript
复制
var foo = {
  something: function () {
    console.log('something');
  }
};

var bar = Object.create(foo);
bar.something(); // "something"

Object.create() 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype.constructor 引用)。

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

我们并不需要 类来创建两个对象之间的关系,只需要通过委托来关联对象就足够了。而 Object.create() 不包含任何“类的诡计”,所以它可以完美地创建我们想要的关联关系。

# 行为委托

[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象。

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

JavaScript 中这个机制的本质就是对象之间的关联关系

# 面向委托的设计

[[Prototype]] 代表的是一种不同于类的设计模式。试着把思路从类和继承的设计模式转换到委托行为的设计模式。

# 类理论

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

如果使用类,那设计方法可能是这样的:定义一个通用父(基)类,可以将其命名为 Task ,在 Task 类中定义所有任务都有的行为。接着定义子类 XYZ 和 ABC ,它们都继承自 Task 并且会添加一些特殊的行为来处理对应的任务。

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

代码语言:javascript
复制
// 伪代码
class Task {
  id;

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

  outputTask () {
    output(id);
  }
}

class XYZ inherits Task {
  label;

  // 构造函数
  XYZ (ID, LABEL) {
    super(ID);
    label = LABEL;
  }
  outputTask () {
    super.outputTask();
    output(label);
  }
}

class ABC inherits Task {
  // ...
}

现在你可以实例化子类 XYZ 的一些副本然后使用这些实例来执行任务 “XYZ”。这些实例会复制 Task 定义的通用行为以及 XYZ 定义的特殊行为。同理,ABC 类的实例也会复制 Task 的行为和 ABC 的行为。在构造完成后,你通常只需要操作这些实例(而不是类),因为每个实例都有你需要完成任务的所有行为。

# 委托理论

使用委托行为 而不是类来思考同样的问题。

首先你会定义一个名为 Task 的对象(和许多 JavaScript 开发者告诉你的不同,它既不是类也不是函数),它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着,对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。你会把特定的任务对象都关联到 Task 功能对象上,让它们在需要的时候可以进行委托。

基本上你可以想象成,执行任务“XYZ”需要两个兄弟对象(XYZ 和 Task)协作完成。但是我们并不需要把这些行为放在一起,通过类的复制,我们可以把它们分别放在各自独立的对象中,需要时可以允许 XYZ 对象委托给 Task 。

代码语言:javascript
复制
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);
// ...

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

相比于面向类(或者说面向对象),可以把这种编码风格称为“对象关联”(OLOO,objects linked to other objects)。我们真正关心的只是 XYZ 对象(和 ABC 对象)委托了 Task 对象。

在 JavaScript 中,[[Prototype]] 机制会把对象关联到其他对象。

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

# 比较

面向对象风格:

代码语言:javascript
复制
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 () {
  alert("Hello, " + this.identify() + ".");
};


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

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

对象关联风格:

代码语言:javascript
复制
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(); // "Hello, I am b1."
b2.speak(); // "Hello, I am b2."

通过比较可以看出,对象关联风格的代码显然更加简洁,因为这种代码只关注一件事:对象之间的关联关系

# 类与对象

代码语言: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) {
  Widget.call(this, width, height);
  this.label = label || "Default";

  this.$elem = $("<button>").text(this.label);
}

// 子类继承父类
Button.prototype = Object.create(Widget.prototype);

// 重写
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 语法糖
代码语言:javascript
复制
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);
});
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021/2/8,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 原型
    • # [[Prototype]]
      • # Object.prototype
        • # 属性设置和屏蔽
          • # “类”
            • # “类” 函数
            • # “构造函数”
          • # (原型)继承
            • # 检查 “类” 关系
          • # 对象关联
            • # 创建关联
        • # 行为委托
          • # 面向委托的设计
            • # 类理论
            • # 委托理论
            • # 比较
          • # 类与对象
            • # ES6 的 class 语法糖
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档