V8 基础

在学习 v8之前首先需要弄清楚一些概念,比如 isolate,handle,handle scope, context,template,以及 v8 对象 和 Blink 中 DOM 对象的关系.

另外还需要了解 isolates, contexts, worlds and frames 之间的关系.

Isolate

isolate 表示一个单独的拥有自己的堆 VM 实例, 每个 isolate 都是完全独立的, 对象在 isolate 之间是不能共享的.嵌入程序可以创建多个 isolate,并且可以在多个线程中使用它们,但是在任何给定的时间,一个 isolate最多只能有一个线程进入其中,多线程必须加锁保证同步.

在 blink 中 isolate 和线程是1:1的关系.主线程与一个 isolate 相关联,一个工作线程与一个 isolate 相关联,但是也有例外 compositer worker 是多个共享一个 isolate.

Handle

Handle 提供了一个JS 对象在堆内存中的地址的引用. V8 垃圾回收器将回收一个已无法被访问到的对象占用的堆内存空间. 垃圾回收过程中, 回收器通常会将对象在堆内存中进行移动. 当回收器移动对象的同时, 也会将所有相应的 Handle 更新为新的地址.

当一个对象在 JavaScript 中无法被访问到, 并且也没有任何 Handle 引用它, 则这个对象将被当作 "垃圾" 对待. 回收器将不断将所有判定为 "垃圾" 的对象从堆内存中移除. V8 的垃圾回收机制是其性能的关键所在.

Handle 又包括很多种,比较常见的有 Local Handles和Persistent handle.

Local Handles 保存在一个栈结构中, 当栈的析构函数(destructor)被调用时将同时被销毁. 这些 handle 的生命周期取决于 handle scope (当一个函数被调用的时候, 对应的 handle scope 将被创建). 当一个 handle scope 被销毁时, 如果在它当中的 handle 所引用的对象已无法再被 JavaScript 访问, 或者没有其他的 handle 指向它, 那么这些对象都将在 scope 的销毁过程中被垃圾回收器回收. 入门指南中的例子使用的就是这种 Handle.

注意:Handle 栈并不是 C++ 调用栈的一部分, 不过 handle scope 是被嵌入到C++栈中的. Handle scope只支持栈分配, 而不能使用 new 进行堆分配.

Persistent handle 是一个堆内存上分配的 JavaScript 对象的引用, 这点和 local handle 一样. 但它有两个自己的特点, 是对于它们所关联的引用的生命周期管理方面. 当你 希望 持有一个对象的引用, 并且超出该函数调用的时期或范围时, 或者是该引用的生命周期与 C++ 的作用域不一致时, 就需要使用 persistent handle 了. 例如 Google Chrome 就是使用 persistent handle 引用 DOM 节点. Persistent handle 支持弱引用, 即 PersistentBase::SetWeak, 它可以在其引用的对象只剩下弱引用的时候, 由垃圾回收器出发一个回调.

  • 一个 UniquePersistent<SomeType> 依赖 C++ 的构造函数和析构函数来管理其引用的对象的生命周期.
  • 当使用构造函数创建一个 Persistent<SomeType> 后, 必须在使用完后显式调用 Persistent::Reset.

Handle Scope

每次创建对象的时候, 都创建一个相应的 local handle 会产生大量的 handle. 此时, handle scope 就派上用处了. 你可以将 handle scope 看作是存有许多 handle 的容器. 当 handle scope 销毁时, 其中的所有 handle 也随即销毁, 这样, 这些 handle 所引用的对象就能够在下一次垃圾回收的时候被恰当的处理了.

当析构函数 HandleScope::~HandleScope 被调用时, handle scope 被删除, 其中的 handle 所引用的对象将在下次 GC 的时候被适当的处理. 垃圾回收器会移除 source_obj 和 script_obj 对象, 因为他们已经不再被任何 handle 引用, 并且在 JS 代码中也无法访问到他们. 而 context handle 即使在离开 handle scope 后也并不会被移除, 因为它是 persistent handle, 只能通过对它显式调用 Reset 才能将其移除.

Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  
  // 稍后需要创建一个临时的 handle,因此需要使用到 handle scope
  EscapableHandleScope handle_scope(isolate);

  // 创建一个新的空数组
  Local<Array> array = Array::New(isolate, 3);

  // 如果数组创建失败,返回空.
  if (array.empty())
    return Local<Array>();

  // 填充值
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  // 通过 Escape 返回数组
  return handle_scope.Escape(array);
}

陷阱: 你无法从一个在 handle scope 中声明的函数中返回一个 local handle. 如果你这么做了, 那么这个 local handle 将在返回前, 首先在 handle scope 的析构函数被调用时被删除. 返回一个 local handle 的正确方法应该是构建一个 EscapableHandleScope 而不是 HandleScope, 并调用其 Escape() 方法, 将你想要返回的 handle 传递给它.

Escape 方法将其参数的值拷贝到一个封闭作用域中, 然后照常删除所有 Local handle, 然后将一个含有指定值的新的 handle 送回给调用方.

Context(上下文)

在 V8 中, 一个 context 就是一个执行环境, 它使得可以在一个 V8 实例中运行相互隔离且无关的 JavaScript 代码. 你必须为你将要执行的 JavaScript 代码显式的指定一个 context.

之所以这样是因为 JavaScript 提供了一些内建的工具函数和对象, 他们可以被 JS 代码所修改. 比如, 如果两个完全无关的 JS 函数都在用同样的方式修改一个 global 对象, 很可能就会出现一个意外的结果.

如果要为所有必须的内建对象创建一个新的执行上下文(context), 在 CPU 时间和内存方面的开销可能会比较大. 然而, V8 的大量缓存可以对其优化, 你创建的第一个 context 可能相对比较耗时, 而接下来的 context 就快捷很多. 这是因为第一个 context 需要创建内建对象并解析内建的 JavaScript 代码. 而后续的 context 只需要为它自己创建内建对象即可, 而不用再解析 JS 代码了. 伴随 V8 的快照 (snapshot) 特性 (通过 build 选项 snapshot=yes 开启, 默认打开), 首次创建 context 的时间将会得到大量优化, 因为快照包含了一个序列化的堆, 其中包含了已解析编译过的内建 JavaScript 代码. 随着垃圾回收, V8 大量的缓存也是其高性能的关键因素.

当你创建一个 context 后, 你可以进出此上下文任意多的次数. 当你在 context A 中时, 还可以再进入 context B. 此时你将进入 B 的上下文中. 当退出 B 时, A 又将成为你的当前 context. 正如下图所展示的那样.

注意, 每个 context 中的内建函数和对象是相互隔离的. 你也可以在创建一个 context 的时候设置一个安全令牌.

在 V8 中使用 context 的动机是, 浏览器中的每个 window 和 iframe 可以拥有一个属于自己的干净的执行环境.

Context是V8中全局变量范围的概念。简单的说,一个Window对象对应于一个Context。例如<iframe>和parent frame的有不同的Window对象,所以不同的frame具有不同的Context。由于每个Context创建了自己的全局变量和作用域,因此<iframe>的全局变量和原型链与parent frame的全局变量和原型链是隔离的.

例子:

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var foo = 1234;
String.prototype.substr =
    function (position, length) { // Hijacks String.prototype.substr
        console.log(length);
        return "hijacked";
    };
</script>
</body></html>

// iframe.html
<script>
console.log(foo);  // undefined
var bar = "aaaa".substr(0, 2);  // Nothing is logged.
console.log(bar);  // "aa"
</script>

总之,每个frame都有一个Window对象。每个Window对象都有一个Context。每个Context都有自己的全局变量范围和原型链。

Entered context and current context

一个isolate会在多个frame中执行JavaScripts,每个frame都有自己的context。这个意思就是一个isolate下的Context是会变化的。换句话说,isolate和Context之间的关系是1:N

这里我们有一个Entered context和current context的概念。要了解差异,您需要了解两种运行时堆栈:

第一个堆栈是JavaScript函数堆栈。该堆栈由V8管理。当一个函数调用另一个函数时,被调用函数入堆栈。当该函数返回时,该函数从堆栈出栈,然后返回到现在位于堆栈顶部的调用函数。每个函数都有一个相关的Context,我们将当前正在运行的函数的上下文(即,堆栈顶部的函数的上下文)称为current context:

看以下例子:

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe.onload = function () {
    iframe.contentWindow.func();
}
</script>
</body></html>

// iframe.html
<script>
function func() {
  ...;
}
</script>

在上面的示例中,在运行func()时,current context指的是<iframe>的context。

第二个堆栈以更粗糙的粒度运行。该堆栈由V8绑定(而不是V8)管理。当V8绑定调用JavaScript时,V8绑定进入context并将context推送到堆栈。 JavaScript开始在context中运行。当JavaScript完成并且控件返回到V8绑定时,V8绑定会从堆栈中弹出上下文。鉴于V8绑定和V8之间的控制可以嵌套(即,V8绑定调用JavaScript,调用V8绑定,调用另一个JavaScript等),这些context形成堆栈。推送和弹出是由任何V8 API完成的,它采用上下文参数或显式调用v8 :: Context :: Enter()和v8 :: Context :: Exit()。我们将最近输入的context称为Entered context。

在上面的示例中,在运行func()时,Entered context是main frame的context(而不是<iframe>的context)。

Entered context是实现HTML规范的条目设置对象的概念。当前上下文是实现HTML规范的现任设置对象的概念。

总之,Entered context是从中开始当前JavaScript执行的context。current context是当前正在运行的JavaScript函数的context。

还有另一个称为调试器上下文的特殊上下文。如果调试器处于活动状态,则可以将调试器上下文插入到上下文堆栈中

World(world 是V8 bingding 中的概念)

World是在Chrome扩展的内容脚本中沙盒DOM wrappers的概念. 这里三种World类型:

  1. main world

  2. isolated world

  3. worker world

main world用于执行网页的JavaScript脚本

isolated world用于执行Chrome扩展程序的内容脚本

主线程的isolate有1个main world和N个isolated worlds

work thread只有1 worker world ,但是没有isolated world

如下图所示:

一个isolate中的所有Worlds共享底层C ++ DOM对象,但每个World都有自己的DOM wrappers 。这样一个隔离区中的世界可以在相同的C ++ DOM对象上运行,而无需在World中共享任何DOM wrapper

每个World都有自己的上下文。这意味着每个World都有自己的全局变量范围和原型链

作为沙盒的结果,一个isolate中的World不能共享任何DOM wrappers 或上下文,但可以共享底层C ++ DOM对象。没有共享DOM wrappers 或上下文这一事实意味着World之间不会共享任何JavaScript对象。这样我们就可以保证Chrome扩展在共享底层C ++ DOM对象时不共享任何JavaScript对象的安全模型。此沙箱允许Chrome扩展在共享DOM结构上运行不受信任的JavaScripts。

(注意: isolated world是V8绑定的概念,而isolate和上下文是V8的概念.V8不知道isolated worlds 是什么。)

总之,主线程的isolate由1个main world和N 个isolated worlds组成。worker thread的isolate 由1个 worker world 和 0 isolated world组成。一个隔离中的所有World共享底层的C ++ DOM对象,但每个World都有自己的DOM wrappers。每个World都有自己的上下文,因此有自己的全局变量范围和原型链。

isolates, contexts, worlds and frames 之间的关系

总结一下isolates,contexts,worlds 和frames之间的关系:

  • DOM端的要求,一个HTML页面具有N个frame。每个frame都有自己的context

  • JavaScript端的要求,一个isolate具有M个world。每个world都有context

结果,当我们执行涉及N Frame 和M个world的主线程时,存在N * M个上下文。换句话说,为每对(frame,world)创建一个上下文。下图有助于理解这种关系:

主线程一次只能有一个current context,但主线程在其生命周期内可以有N * M个上下文。例如,当主线程使用World Y中的JavaScript在frame X上操作时,当前上下文被设置为(X,Y)对的上下文。主线程的当前上下文在其生命周期中发生变化。

另一方面,work thread 有0 frame 和1 World。因此,工作线程只有1个context。工作线程的current context永远不会改变。

DOM wrappers and contexts

出于兼容性原因,只要底层C ++ DOM对象处于活动状态,我们就需要确保将相同的DOM wrapper 返回给JavaScript。我们不应该为同一个C ++ DOM对象返回不同的DOM包装器。

以下是代码例子

var div = document.createElement("div");
div.foo = 1234; // expando
var p = document.createElement("p");
p.appendChild(div);
div = null;
gc();
console.log(p.firstChild.foo); // This should be 1234, not undefined
要实现只要底层C ++ DOM对象处于活动状态,同一DOM wrapper 返回到JavaScript的语义,我们需要从C ++ DOM对象到DOM包装器的映射。另外,我们需要在每个world中沙箱DOM wrapper 。为了满足这些要求,我们让每个world都拥有一个DOM wrapper 存储,它存储从C ++ DOM对象到该世界中DOM wrapper 的映射。

因此,我们在一个隔离中有多个DOM wrapper 存储。main World 的映射用ScriptWrappable编写。如果ScriptWrappable :: main_world_wrapper_具有非空值,则它是main World 的C ++ DOM对象的DOM wrapper 。其他world的映射是在DOMWrapperMap中编写的。

DOM wrappers and contexts

创建新的DOM wrapper 时,需要选择创建DOM wrapper 的正确上下文。如果在错误的上下文中创建新的DOM包装器,最终会将JavaScript对象泄漏到其他上下文,这很可能会导致安全问题。

// main.html
<html><body>
<iframe src="iframe.html"></iframe>
<script>
var iframe = document.querySelector("iframe");
iframe;  // The wrapper of the iframe should be created in the context of the main frame.
iframe.contentDocument;  // The wrapper of the document should be created in the context of the iframe.
iframe.contentDocument.addEventListener("click",
    function (event) {  // The wrapper of the event should be created in the context of the iframe.
        event.target;
    });
</script>
</body></html>

// iframe.html
<script>
</script>

要确保在正确的上下文中创建DOM wrapper ,您需要确保在调用ToV8()时必须将当前上下文设置为正确的上下文。

template

在一个 context 中, template 是 JavaScript 函数和对象的一个模型. 你可以使用 template 来将 C++ 函数和数据结构封装在一个 JavaScript 对象中, 这样它就可以被 JS 代码操作. 例如, Chrome 使用 template 将 C++ DOM 节点封装成 JS 对象, 并且将函数安装在 global 命名空间中. 你可以创建一个 template 集合, 在每个创建的 context 中你都可以重复使用它们. 你可以按照你的需求, 创建任意多的 template. 然而在任意一个 context 中, 任意 template 都只能拥有一个实例.

在 JS 中, 函数和对象之间有很强的二元性. 在 C++ 或 Java 中创建一种新的对象类型通常要定义一个类. 而在 JS 中你却要创建一个函数, 并以函数为构造器生成对象实例. JS 对象的内部结构和功能很大程度上是由构造它的函数决定的. 这些也反映在 V8 的 template 的设计中, 因此 V8 有两种类型的 template:

FunctionTemplate

一个 Function Template 就是一个 JS 函数的模型. 我们可以在我们指定的 context 下通过调用 template 的 GetFunction 方法来创建一个 JS 函数的实例. 你也可以将一个 C++ 回调与一个当 JS 函数实例执行时被调用的 function template 关联起来.

ObjectTemplate

每一个 Function Template 都与一个 Object Template 相关联. 它用来配置以该函数作为构造器而创建的对象. 你也可以给这个 Object Template 关联两类 C++ 回调:

存取器回调. 当指定的对象属性被 JS 访问时调用.

拦截器回调. 当任意对象属性被访问时调用.

// 为 global 对象创建一个 template 并设置内建全局函数.
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));

// 每个任务都有属于自己的 context, 所以不同的任务相互之间不影响.
Persiitent<Context> context = Context::New(isolate, NULL, global);

Accessors (存取器)

存取器是一个当对象属性被 JS 代码访问的时候计算并返回一个值的 C++ 回调. 存取器是通过 Object Template 的 SetAccessor 方法进行配置的. 该方法接收属性的名称和与其相关联的回调函数, 分别在 JS 读取和写入该属性时触发.

存取器的复杂性源于你所操作的数据的访问方式:

  • 访问静态全局变量
  • 访问动态变量

Accessing Static Global Variables (访问静态全局变量)

假设有两个 C++ 整数变量 x 和 y, 要让他它们可以在 JS 中通过 global 对象进行访问. 我们需要在 JS 代码读写这些变量的时候调用相应的 C++ 存取器函数. 这些存取函数将一个 C++ 整数通过 Integer::New 转换成 JS 整数, 并将 JS 整数转换成32位 C++ 整数. 来看下面的例子:

void XGetter(Local<String> property, const PropertyCallbackInfo<Value>& info) {
  info.GetReturnValue().Set(x);
}

void XSetter(Local<String> property, Local<Value> value,
             const PropertyCallbackInfo<Value>& info) {
  x = value->Int32Value();
}

void YGetter(Local<String> property, const PropertyCallbackInfo<Value>& info) {
  info.GetReturnValue().Set(x);
}

void YSetter(Local<String> property, Local<Value> value,
             const PropertyCallbackInfo<Value>& info) {
  x = value->Int32Value();
}

Local<ObjectTemplate> global_templ = ObjectTemplate::New(isolate);
global_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), XGetter, XSetter);
global_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), YGetter, YSetter);
Persistent<Context> context = Context::New(isolate, NULL, global_templ);

注意上述代码中的 Object Template 是和 context 同时创建的. 事实上 Template 可以提前创建好, 并可以在任意 context 中使用.

Accessing Dynamic Variables (访问动态变量)

为了让任意多个 C++ Point 实例在 JS 中可用, 我们需要为每一个 C++ Point 创建一个 JS 对象, 并将它们联系起来. 这可以通过外部值和内部成员实现.
首先为 point 创建一个 Object template 封装对象:

Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);

每个 JS point 对象持有一个 C++ 封装对象的引用, 封装对象中有一个 Internal Field, 之所以这么叫是因为它们无法在 JS 中访问, 而只能通过 C++ 代码访问. 一个对象可以有任意多个 Internal Field, 其数量可以按以下方式在 Object Template 上设置.

point_templ->SetInternalFieldCount(1);

此处的 internal field count 设置为了 1, 这表示该对象有一个 internal field, 其 index 是 0, 指向一个 C++ 对象.

将 x 和 y 存取器添加到 template 上:

point_templ->SetAccessor(String::NewFromUtf8(isolate, "x"), GetPointX, SetPointX);
point_templ->SetAccessor(String::NewFromUtf8(isolate, "y"), GetPointY, SetPointY);

接下来通过创建一个新的 template 实例来封装一个 C++ point, 将封装对象的 interanl field 设置为 0.

Point* p = ...;
Local<Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, External::New(isolate, p));

以上代码中, 外部对象就是一个 void* 的封装体. 外部对象只能用来在 internal field 上存储引用值. JS 对象无法直接引用 C++ 对象, 因此可以将外部值当作是一个从 JS 到 C++ 的桥梁. 从这种意义上来说, 外部值是和 handle 相对的概念( handle 是 C++ 到 JS 对象的引用 ).
以下是 x 的存取器的定义, y 的和 x 一样.

void GetPointX(Local<String> property, const PropertyCallbackInfo& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  int value = static_cast<Point*>(ptr)->x_;
  info.GetReturnValue().Set(value);
}

void SetPointX(Local<String> property, Local<Value> value,
               const PropertyCallbackInfo<Value>& info) {
  Local<Object> self = info.Holder();
  Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
  void* ptr = wrap->Value();
  static_cast<Point*>(ptr)->x_ = value->Int32Value();
}

存取器抽取出了被 JS 对象封装起来的 point 对象的引用, 并读写相关联的成员. 这样, 这些通用的存取器就可以被所有封装后的 point 对象使用了.

Interceptors (拦截器)

我们可以设置一个回调, 让它在对应对象的任意属性被访问时都会被调用. 这就是 Interceptor. 考虑到效率, 分为两种不同的 interceptor:

  • 属性名拦截器: 当通过字符串形式的属性名访问时调用. 比如在浏览器中使用 document.theFormName.elementName 进行访问.
  • 属性索引拦截器: 当通过属性的下标/索引访问时调用. 比如在浏览器中使用 document.forms.elements[0] 进行访问.

V8 源码 process.cc 的代码中, 包含了一个使用 interceptor 的例子. 在下面的代码片段中, SetNamedPropertyHandler 指定了 MapGet 和 MapSet 两个 interceptor:

Local<ObjectTemplate> result = ObjectTemplate::New(isolate);
result->SetNamePropertyHandler(MapGet, MapSet);

void JsHttpRequestProcessor::MapGet(Local<String> name,
                                    const PropertyCallbackInfo<Value>& info) {
  // Fetch the map wrapped by this object.
  std::map<string, string>* obj = UnwrapMap(info.Holder());
  
  // Convert the JavaScript string to a std::string.
  string key = ObjectToString(name);

  // Look up the value if it exists using the standard STL idiom.
  map<string, string>::iterator iter = obj->find(key);

  if (iter == obj->end()) return;

  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string& value = (*iter).second;
  info.GetReturnValue().Set(String::NewFromUtf8(value.c_str(),
                            String::kNormalString, value.length()));
}

Security Model (安全模型)

同源策略用来防止从一个源载入的文档或脚本存取另外一个源的文档. 这里所谓的 "同源" 是指相同的 protocal + domain + port, 这三个都相同的两个网页才被认为是同源. 如果没有它的保护, 恶意网页将危害到其他网页的完整性.

在 V8 中, 同源被定义为相同的 context. 默认情况下, 是无法访问别的 context 的. 如果一定要这样做, 需要使用安全令牌或安全回调. 安全令牌可以是任意值, 但通常来说是个唯一的规范字符串. 当建立一个 context 时, 我们可以通过 SetSecurityToken 来指定一个安全令牌, 否则 V8 将自动为该 context 生成一个.

当试图访问一个全局变量时, V8 安全系统将先检查该全局对象的安全令牌, 并将其和试图访问该对象的代码的安全令牌比对. 如果匹配则放行, 否则 V8 将触发一个回调来判断是否应该放行. 我们可以通过 object template 上的 SetAccessCheckCallbacks 方法来定义该回调来并决定是否放行. V8 安全系统可以用被访问对象上的安全回调来判断访问者的 context 是否有权访问. 该回调需要传入被访问的对象, 被访问的属性以及访问的类型(例如读, 写, 或删除), 返回结果为是或否.

Chrome 实现了这套机制, 对于安全令牌不匹配的情况, 只有以下这些才可以通过安全回调的方式来判断是否可以放行: window.focus(), window.blur(), window.close(), window.location, window.open(), history.forward(), history.back(), 和 history.go().

Exceptions (异常)

如果发生错误, V8 会抛出异常. 比如, 当一个脚本或函数试图读取一个不存在的属性时, 或者一个不是函数的值被当作函数进行调用执行时.

如果一个操作不成功, V8 将返回一个空的 handle. 因此我们应该在代码中检查返回值是否是一个空的 handle, 可以使用 Local 类的公共成员函数 isEmpty() 来检查 handle 是否为空.

我们也可以像以下示例一样 Try Catch 代码中发生的异常:

TryCatch trycatch(isolate);
Local<Value> v = script->Run();
if (v.IsEmpty()) {
  Local<Value> exception = trycatch.Exception();
  String::Utf8Value exception_str(exception);
  printf("Exception: %s\n", *exception_str);
}

如果 value 以一个空 handle 返回, 而你没有 TryCatch 它, 你的程序挂掉, 反之则可以继续执行.

Inheritance (继承)

JS 是一个无类的面向对象编程语言, 因此, 它使用原型继承而不是类继承. 这会让那些接受传统面向对象语言(比如 C++ 和 Java)训练的程序员感到迷惑.

基于类的面向对象编程语言, 比如 C++ 和 Java, 是建立在两种完全不同实体的概念上的: 类和实例. 而 JS 是基于原型的语言, 因此没有这些区别, 它只有对象. JS 本身并不原生支持 类这个层级的声明; 然而, 它的原型机制简化了给对象实例添加自定义属性或方法的过程. 在 JS 中, 你可以像以下代码这样给对象添加属性:

// Create an object "bicycle"
function bicycle() {
  // Do some thing.
}

// Create an instance of bicycle called roadbike
var roadbike = new bicycle();
// Define a custom property =, wheels, on roadbike
roadboke.wheels = 2;

这种方式定义的属性只存在于该对象实例上. 如果创建另一个 bicycle() 实例则其并没有 wheels 属性, 进行访问将返回 undefined. 除非显式的将 wheels 属性添加上去.

有时这正是我们所需要的, 但有时我们希望将属性添加到所有这些实例上去, 这是 JS 的 prototype 对象就派上用处了. 为了使用原型对象, 可以通过 prototype 关键词访问对象原型, 然后在它上面添加自定义的属性:

// First, create the "bicycle" object.
function bicycle() {
}

// Assign the wheels property to the objects's prototype.
bicycle.prorotype.wheels = 2;

此后, 所有 bicycle() 的实例都将预置该属性值了.

V8 通过 template 可以使用同样的方法. 每个 FunctionTemplate 都有一个 PrototypeTemplate 方法可以返回该函数的原型. 我们可以给它设置属性, 也可以将 C++ 函数关联到这些属性, 然后所有该 FunctionTemplate 对应的实例上都将有这些属性和对应的值或函数:

Local<FunctionTemplate> biketemplate = FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
    String::NewFromUtf8(isolate, "wheels"),
    FunctionTemplate::New(isolate, MyWheelsMethodCallback)->GetFunction());

以上代码将使所有 biketemplate 的原型链上都具有 wheels 方法, 当在对应实例上调用 wheels 方法时, MyWheelsMethodCallback 将被执行.

V8 的 FunctionTemplate 类提供了公共的成员函数 Inherit(), 当我们希望当前 function template 继承另外一个 function template 的时候可以调用该方法:

void Inherit(Local<FunctionTemplate> parent);

参考:
https://zhuanlan.zhihu.com/p/54135666
https://github.com/Chunlin-Li/Chunlin-Li.github.io/blob/master/blogs/javascript/V8_Embedder's_Guide_CHS.md
https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/bindings/core/v8/V8BindingDesign.mdhttps://v8.dev/docs/embed

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容