V8嵌入指南

中英文原文链接:https://github.com/Chunlin-Li/Chunlin-Li.github.io/blob/master/blogs/javascript/V8_Embedder's_Guide_CHS.md

简介

如果你已经阅读了入门指南, 一定知道 V8 是一个独立运行的虚拟机, 其中有些关键性概念比如: handles, scopes 和 contexts. 本文将深入讨论这些概念, 并且引入一些其他对于将 c++ 程序嵌入到 V8 中非常重要的信息.V8 API 为编译和执行JS脚本, 访问 C++ 方法和数据结构, 错误处理, 开启安全检查等提供了函数接口. 你的 C++ 应用可以像使用其他 C++ 库一样使用 V8, 只需要在你的代码中 include V8 头文件(include/v8.h).如果你需要优化你的V8应用, "V8 设计元素"一文将会为你提供一些有用的背景知识.
  本文是为那些想要将 V8 JavaScript 引擎嵌入到 C++ 程序中的工程师而撰写. 它将帮你搭建 C++ 程序的对象和方法与 JavaScript 对象和函数之间的桥梁.

1、Handles and Garbage Collection (Handle 与 GC 垃圾)

Handle 提供了一个JS 对象在堆内存中的地址的引用. V8 垃圾回收器将回收一个已无法被访问到的对象占用的堆内存空间. 垃圾回收过程中, 回收器通常会将对象在堆内存中进行移动. 当回收器移动对象的同时, 也会将所有相应的 Handle 更新为新的地址.当一个对象在 JavaScript 中无法被访问到, 并且也没有任何 Handle 引用它, 则这个对象将被当作 "垃圾" 对待. 回收器将不断将所有判定为 "垃圾" 的对象从堆内存中移除. V8 的垃圾回收机制是其性能的关键所在. 更多相关信息见 "V8 设计元素" 一文.
  Local Handles 保存在一个栈结构中, 当栈的析构函数(destructor)被调用时将同时被销毁. 这些 handle 的生命周期取决于 handle scope (当一个函数被调用的时候, 对应的 handle scope 将被创建). 当一个 handle scope 被销毁时, 如果在它当中的 handle 所引用的对象已无法再被 JavaScript 访问, 或者没有其他的 handle 指向它, 那么这些对象都将在 scope 的销毁过程中被垃圾回收器回收. 入门指南中的例子使用的就是这种 Handle.
  Local handle 对应的类是 Local<SomeType>
  注意 : 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.
  • Eternal 是一个用于预期永远不会被释放的 JavaScript 对象的 persistent handle, 使用它的代价更小, 因为它减轻了垃圾回收器判定对象是否存活的负担.
  • Persistent 和 UniquePersistent 都无法被拷贝, 使得它无法成为 C++11 之前的标准库容器的值. PersistentValueMap 和 PersistentValueVector 为persistent 值提供了容器类, 并且带有 Map 或类 Vector 的语义. C++11 的开发者不需要他们, 因 C++11 改变了语义, 解决了潜在的问题.
  • 当然, 每次创建对象的时候, 都创建一个相应的 local handle 会产生大量的 handle. 此时, handle scope 就派上用处了. 你可以将 handle scope 看作是存有许多 handle 的容器. 当 handle scope 销毁时, 其中的所有 handle 也随即销毁, 这样, 这些 handle 所引用的对象就能够在下一次垃圾回收的时候被恰当的处理了.
      回到我们在入门指南中的简单示例上 , 在下面这张图表中你可以看到 handle-stack 和在堆内存上分配的对象. 注意, Context::New() 将返回一个 Local handle, 基于它, 我们创建了一个新的 Persistent handle 来演示 Persistent handle 的用法.
  • 当析构函数 HandleScope::~HandleScope 被调用时, handle scope 被删除, 其中的 handle 所引用的对象将在下次 GC 的时候被适当的处理. 垃圾回收器会移除 source_obj 和 script_obj 对象, 因为他们已经不再被任何 handle 引用, 并且在 JS 代码中也无法访问到他们. 而 context handle 即使在离开 handle scope 后也并不会被移除, 因为它是 persistent handle, 只能通过对它显式调用 Reset 才能将其移除.
    注意: 整篇文档中的名词 handle 都表示 local handle, 如果要表示 persistent handle 会使用全称.
// This function returns a new array with three elements, x, y, and z.  
// 该函数返回一个新数组, 其中包含 x, y, z 三个元素  
Local<Array> NewPointArray(int x, int y, int z) {  
  v8::Isolate* isolate = v8::Isolate::GetCurrent();  
  
  // We will be creating temporary handles so we use a handle scope.  
  // 我们稍后需要创建一个临时 handle, 因此我们需要使用 handle scope.  
  EscapableHandleScope handle_scope(isolate);  
  
  // Create a new empty array.  
  // 创建一个新的空数组.  
  Local<Array> array = Array::New(isolate, 3);  
  
  // Return an empty result if there was an error creating the array.  
  // 如果创建数组失败, 返回空.  
  if (array.IsEmpty())  
    return Local<Array>();  
  
  // Fill out the values  
  // 填充值  
  array->Set(0, Integer::New(isolate, x));  
  array->Set(1, Integer::New(isolate, y));  
  array->Set(2, Integer::New(isolate, z));  
  
  // Return the value through Escape.  
  // 通过 Escape 返回数组  
  return handle_scope.Escape(array);  
}  

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

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

2、Contexts (上下文)##

  • 在 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 大量的缓存也是其高性能的关键因素, 更多信息请参阅 "V8 设计元素"一文.
  • 当你创建一个 context 后, 你可以进出此上下文任意多的次数. 当你在 context A 中时, 还可以再进入 context B. 此时你将进入 B 的上下文中. 当退出 B 时, A 又将成为你的当前 context. 正如下图所展示的那样.
    注意, 每个 context 中的内建函数和对象是相互隔离的. 你也可以在创建一个 context 的时候设置一个安全令牌. 更多信息请参阅安全模型一节.
    在 V8 中使用 context 的动机是, 浏览器中的每个 window 和 iframe 可以拥有一个属于自己的干净的执行环境.

3、Templates (模板)##

在一个 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 访问时调用.
  • 拦截器回调. 当任意对象属性被访问时调用.
    存取器和拦截器将在后面的部分讲到.
// Create a template for the global object and set the  
// built-in global functions.  
// 为 global 对象创建一个 template 并设置内建全局函数  
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);  
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));  
  
// Each processor gets its own context so different processors  
// do not affect each other.  
// 每个任务都有属于自己的 context, 所以不同的任务相互之间不影响.  
Persistent<Context> context = Context::New(isolate, NULL, global);  
This example code is taken from JsHttpProcessor::Initializer in the process.cc sample.  
  
以上代码提供了一个 为 global 对象创建 tamplate 并设置内建全局函数的例子.  
  
示例代码是 process.cc 中 JsHttpProcessor::Initializer 的片段.  

4、Accessors (存取器)##

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

  • 访问静态全局变量
  • 访问动态变量
  • 假设有两个 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();  
  }  
  
  // YGetter/YSetter are so similar they are omitted for brevity  
  
  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 中使用.

5、Accessing Dynamic Variables (访问动态变量)##

在前一个例子中, 变量是静态全局的, 那么如果是一个动态操纵的呢? 比如用于标记一个 DOM 树是否在浏览器中的变量? 我们假设 x 和 y 是 C++ 类 Point 上的成员:

class Point {  
   public:  
    Point(int x, int y) : x_(x), y_(y) { }  
    int x_, y_;  
  }  

为了让任意多个 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<Value>& 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();  
  }  

6、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->SetNamedPropertyHandler(MapGet, MapSet);  
The MapGet interceptor is provided below:  
  
void JsHttpRequestProcessor::MapGet(Local<String> name,  
                                    const PropertyCallbackInfo<Value>& info) {  
  // Fetch the map wrapped by this object.  
  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 the key is not present return an empty handle as signal.  
  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()));  
}  

和存取器一样, 对应的回调将在每次属性被访问的时候调用, 只不过拦截器会处理所有的属性, 而存取器只针对相关联的属性.

7、Security Model (安全模型)##

同源策略用来防止从一个源载入的文档或脚本存取另外一个源的文档. 这里所谓的 "同源" 是指相同的 protocal + domain + port, 这三个都相同的两个网页才被认为是同源. 如果没有它的保护, 恶意网页将危害到其他网页的完整性.
  同源策略在 Netscape Navigator 2.0 中首次引入
  在 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()

8、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 它, 你的程序挂掉, 反之则可以继续执行.

9、Inheritance (继承)##

JS 是一个无类的面向对象编程语言, 因此, 它使用原型继承而不是类继承. 这会让那些接受传统面向对象语言(比如 C++ 和 Java)训练的程序员感到迷惑.基于类的面向对象编程语言, 比如 C++ 和 Java, 是建立在两种完全不同实体的概念上的: 类和实例. 而 JS 是基于原型的语言, 因此没有这些区别, 它只有对象. JS 本身并不原生支持 类这个层级的声明; 然而, 它的原型机制简化了给对象实例添加自定义属性或方法的过程. 在 JS 中, 你可以像以下代码这样给对象添加属性:

// Create an object "bicycle"   
function bicycle(){   
}   
// Create an instance of bicycle called roadbike  
var roadbike = new bicycle()  
// Define a custom property, wheels, on roadbike   
roadbike.wheels = 2  

这种方式定义的属性只存在于该对象实例上. 如果创建另一个 bicycle() 实例则其并没有 wheels 属性, 进行访问将返回 undefined. 除非显式的将 wheels 属性添加上去.
  有时这正是我们所需要的, 但有时我们希望将属性添加到所有这些实例上去, 这是 JS 的 prototype 对象就派上用处了. 为了使用原型对象, 可以通过 prototype 关键词访问对象原型, 然后在它上面添加自定义的属性:

function bicycle(){   
}  
// Assign the wheels property to the object's prototype  
bicycle.prototype.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);

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

推荐阅读更多精彩内容

  • V8 是一个独立运行的虚拟机, 其中有些关键性概念比如: handles, scopes 和 contexts. ...
    转角遇见一直熊阅读 8,608评论 1 8
  • 目录 1.静态作用域与动态作用域 2.变量的作用域 3.JavaScript 中变量的作用域 4.JavaScri...
    一缕殇流化隐半边冰霜阅读 7,033评论 37 113
  • JavaScript绝对是最火的编程语言之一,一直具有很大的用户群,随着在服务端的使用(NodeJs),更是爆发了...
    不去解释阅读 2,347评论 1 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 很实用的编程英语词库,共收录一千五百余条词汇。 第一部分: application 应用程式 应用、应用程序app...
    春天的蜜蜂阅读 1,272评论 0 22