js引擎v8源码分析之Object(基于v8 0.1.5)

Object是所有js对象在c++层的基类。

class Object BASE_EMBEDDED {
 public:
 
  inline bool IsSmi();
  inline bool IsHeapObject();
  inline bool IsHeapNumber();
  inline bool IsString();
  inline bool IsSeqString();
  inline bool IsAsciiString();
  inline bool IsTwoByteString();
  inline bool IsConsString();
  inline bool IsSlicedString();
  inline bool IsExternalString();
  inline bool IsExternalAsciiString();
  inline bool IsExternalTwoByteString();
  inline bool IsShortString();
  inline bool IsMediumString();
  inline bool IsLongString();
  inline bool IsSymbol();
  inline bool IsNumber();
  inline bool IsByteArray();
  inline bool IsFailure();
  inline bool IsRetryAfterGC();
  inline bool IsException();
  inline bool IsJSObject();
  inline bool IsMap();
  inline bool IsFixedArray();
  inline bool IsDescriptorArray();
  inline bool IsContext();
  inline bool IsGlobalContext();
  inline bool IsJSFunction();
  inline bool IsCode();
  inline bool IsOddball();
  inline bool IsSharedFunctionInfo();
  inline bool IsJSValue();
  inline bool IsProxy();
  inline bool IsBoolean();
  inline bool IsJSArray();
  inline bool IsHashTable();
  inline bool IsDictionary();
  inline bool IsSymbolTable();
  inline bool IsPrimitive();
  inline bool IsGlobalObject();
  inline bool IsJSGlobalObject();
  inline bool IsJSBuiltinsObject();
  inline bool IsUndetectableObject();
  inline bool IsAccessCheckNeeded();

  // Returns true if this object is an instance of the specified
  // function template.
  bool IsInstanceOf(FunctionTemplateInfo* type);

  inline bool IsStruct();
  inline bool IsAccessorInfo(); 
  inline bool IsAccessCheckInfo(); 
  inline bool IsInterceptorInfo(); 
  inline bool IsCallHandlerInfo(); 
  inline bool IsFunctionTemplateInfo(); 
  inline bool IsObjectTemplateInfo(); 
  inline bool IsSignatureInfo(); 
  inline bool IsTypeSwitchInfo(); 
  inline bool IsDebugInfo(); 
  inline bool IsBreakPointInfo(); 
  inline bool IsScript();

  // Oddball testing.
  INLINE(bool IsUndefined());
  INLINE(bool IsTheHole());
  INLINE(bool IsNull());
  INLINE(bool IsTrue());
  INLINE(bool IsFalse());

  // Extract the number.
  inline double Number();

  Object* ToObject();             // ECMA-262 9.9.
  Object* ToBoolean();            // ECMA-262 9.2.

  Object* ToObject(Context* global_context);

  inline Object* ToSmi();

  void Lookup(String* name, LookupResult* result);

  // Property access.
  inline Object* GetProperty(String* key);
  inline Object* GetProperty(String* key, PropertyAttributes* attributes);
  Object* GetPropertyWithReceiver(Object* receiver,String* key,PropertyAttributes* attributes);
  Object* GetProperty(Object* receiver,LookupResult* result,String* key,PropertyAttributes* attributes);
  Object* GetPropertyWithCallback(Object* receiver,Object* structure,String* name,Object* holder);

  inline Object* GetElement(uint32_t index);
  Object* GetElementWithReceiver(Object* receiver, uint32_t index);

  Object* GetPrototype();

  inline bool IsStringObjectWithCharacterAt(uint32_t index);

  static Object* cast(Object* value) { return value; }

  // 对象在内存的布局,基类没有属性,内存是0
  static const int kSize = 0;  // Object does not take up any space.

 private:
  // 禁止直接创建对象,复制函数,赋值函数
  DISALLOW_IMPLICIT_CONSTRUCTORS(Object);
};

基类主要是提供一些公共的方式,比如判断类型,属性存取。类型转化。

1 c++对象的类型
1 v8的对象是4字节对齐的,用地址的低两位出来标记对象的类型。
2 堆对象(HeapObject)是Object的子类。Object里面的很多方法都是用于堆对象。堆对象有自己的一套对象类型判断方式。每个堆对象有一个map属性,他记录了堆对象的类型type,大小size。
1 type主要是用于记录c++对象的类型。
2 有一些单例的map对象,也是用于判断c++对象的类型的。
3 map的type属性的低八位是用来区分是不是字符串类型的。高位8位是1说明不是字符串,是0说明是字符串类型。低7位记录字符串的子类型。
下面是定义。

// 第7位(从0开始算)如果是1说明对象不是字符串类型,否则是 
const uint32_t kIsNotStringMask = 0x80;
const uint32_t kStringTag = 0x0;
const uint32_t kNotStringTag = 0x80;

// If bit 7 is clear, bits 5 and 6 are the string's size (short, medium, or
// long).
// 对于字符串类型,5,6两位标记字符串的类型,短,中等,长三种类型
const uint32_t kStringSizeMask = 0x60;
const uint32_t kShortStringTag = 0x0;
const uint32_t kMediumStringTag = 0x20;
const uint32_t kLongStringTag = 0x40;

// If bit 7 is clear, bit 4 indicates that the string is a symbol (if set) or
// not (if cleared).
// 第四位标记字符串是不是symbol类型,1则表示是
const uint32_t kIsSymbolMask = 0x10;
const uint32_t kNotSymbolTag = 0x0;
const uint32_t kSymbolTag = 0x10;

// If bit 7 is clear, and the string representation is a sequential string,
// then bit 3 indicates whether the string consists of two-byte characters or
// one-byte characters.
// 第三位表示字符串是单字节字符还是双字节字符组成的
const uint32_t kStringEncodingMask = 0x8;
const uint32_t kTwoByteStringTag = 0x0;
const uint32_t kAsciiStringTag = 0x8;

// If bit 7 is clear, the low-order 3 bits indicate the representation
// of the string.
// 第0,1,2位表示字符串类型,四种
const uint32_t kStringRepresentationMask = 0x07;
enum StringRepresentationTag {
  kSeqStringTag = 0x0,
  kConsStringTag = 0x1,
  kSlicedStringTag = 0x2,
  kExternalStringTag = 0x3
};

enum InstanceType {
  // 下面都是字符串类型,根据上面的定义,下面的tag标记在8比特上各有自己的位置范围,所以相与的结果不会一样
  SHORT_SYMBOL_TYPE = kShortStringTag | kSymbolTag | kSeqStringTag,
  MEDIUM_SYMBOL_TYPE = kMediumStringTag | kSymbolTag | kSeqStringTag,
  LONG_SYMBOL_TYPE = kLongStringTag | kSymbolTag | kSeqStringTag,
  SHORT_ASCII_SYMBOL_TYPE = kShortStringTag | kAsciiStringTag | kSymbolTag | kSeqStringTag,
  MEDIUM_ASCII_SYMBOL_TYPE = kMediumStringTag | kAsciiStringTag | kSymbolTag | kSeqStringTag,
  LONG_ASCII_SYMBOL_TYPE = kLongStringTag | kAsciiStringTag | kSymbolTag | kSeqStringTag,
  SHORT_CONS_SYMBOL_TYPE = kShortStringTag | kSymbolTag | kConsStringTag,
  MEDIUM_CONS_SYMBOL_TYPE = kMediumStringTag | kSymbolTag | kConsStringTag,
  LONG_CONS_SYMBOL_TYPE = kLongStringTag | kSymbolTag | kConsStringTag,
  SHORT_CONS_ASCII_SYMBOL_TYPE = kShortStringTag | kAsciiStringTag | kSymbolTag | kConsStringTag,
  MEDIUM_CONS_ASCII_SYMBOL_TYPE = kMediumStringTag | kAsciiStringTag | kSymbolTag | kConsStringTag,
  LONG_CONS_ASCII_SYMBOL_TYPE = kLongStringTag | kAsciiStringTag | kSymbolTag | kConsStringTag,
  SHORT_SLICED_SYMBOL_TYPE = kShortStringTag | kSymbolTag | kSlicedStringTag,
  MEDIUM_SLICED_SYMBOL_TYPE = kMediumStringTag | kSymbolTag | kSlicedStringTag,
  LONG_SLICED_SYMBOL_TYPE = kLongStringTag | kSymbolTag | kSlicedStringTag,
  SHORT_SLICED_ASCII_SYMBOL_TYPE = kShortStringTag | kAsciiStringTag | kSymbolTag | kSlicedStringTag,
  MEDIUM_SLICED_ASCII_SYMBOL_TYPE = kMediumStringTag | kAsciiStringTag | kSymbolTag | kSlicedStringTag,
  LONG_SLICED_ASCII_SYMBOL_TYPE = kLongStringTag | kAsciiStringTag | kSymbolTag | kSlicedStringTag,
  SHORT_EXTERNAL_SYMBOL_TYPE = kShortStringTag | kSymbolTag | kExternalStringTag,
  MEDIUM_EXTERNAL_SYMBOL_TYPE = kMediumStringTag | kSymbolTag | kExternalStringTag,
  LONG_EXTERNAL_SYMBOL_TYPE = kLongStringTag | kSymbolTag | kExternalStringTag,
  SHORT_EXTERNAL_ASCII_SYMBOL_TYPE = kShortStringTag | kAsciiStringTag | kSymbolTag | kExternalStringTag,
  MEDIUM_EXTERNAL_ASCII_SYMBOL_TYPE = kMediumStringTag | kAsciiStringTag | kSymbolTag | kExternalStringTag,
  LONG_EXTERNAL_ASCII_SYMBOL_TYPE = kLongStringTag | kAsciiStringTag | kSymbolTag | kExternalStringTag,
  SHORT_STRING_TYPE = kShortStringTag | kSeqStringTag,
  MEDIUM_STRING_TYPE = kMediumStringTag | kSeqStringTag,
  LONG_STRING_TYPE = kLongStringTag | kSeqStringTag,
  SHORT_ASCII_STRING_TYPE = kShortStringTag | kAsciiStringTag | kSeqStringTag,
  MEDIUM_ASCII_STRING_TYPE = kMediumStringTag | kAsciiStringTag | kSeqStringTag,
  LONG_ASCII_STRING_TYPE = kLongStringTag | kAsciiStringTag | kSeqStringTag,
  SHORT_CONS_STRING_TYPE = kShortStringTag | kConsStringTag,
  MEDIUM_CONS_STRING_TYPE = kMediumStringTag | kConsStringTag,
  LONG_CONS_STRING_TYPE = kLongStringTag | kConsStringTag,
  SHORT_CONS_ASCII_STRING_TYPE = kShortStringTag | kAsciiStringTag | kConsStringTag,
  MEDIUM_CONS_ASCII_STRING_TYPE = kMediumStringTag | kAsciiStringTag | kConsStringTag,
  LONG_CONS_ASCII_STRING_TYPE = kLongStringTag | kAsciiStringTag | kConsStringTag,
  SHORT_SLICED_STRING_TYPE = kShortStringTag | kSlicedStringTag,
  MEDIUM_SLICED_STRING_TYPE = kMediumStringTag | kSlicedStringTag,
  LONG_SLICED_STRING_TYPE = kLongStringTag | kSlicedStringTag,
  SHORT_SLICED_ASCII_STRING_TYPE = kShortStringTag | kAsciiStringTag | kSlicedStringTag,
  MEDIUM_SLICED_ASCII_STRING_TYPE = kMediumStringTag | kAsciiStringTag | kSlicedStringTag,
  LONG_SLICED_ASCII_STRING_TYPE = kLongStringTag | kAsciiStringTag | kSlicedStringTag,
  SHORT_EXTERNAL_STRING_TYPE = kShortStringTag | kExternalStringTag,
  MEDIUM_EXTERNAL_STRING_TYPE = kMediumStringTag | kExternalStringTag,
  LONG_EXTERNAL_STRING_TYPE = kLongStringTag | kExternalStringTag,
  SHORT_EXTERNAL_ASCII_STRING_TYPE = kShortStringTag | kAsciiStringTag | kExternalStringTag,
  MEDIUM_EXTERNAL_ASCII_STRING_TYPE = kMediumStringTag | kAsciiStringTag | kExternalStringTag,
  LONG_EXTERNAL_ASCII_STRING_TYPE = kLongStringTag | kAsciiStringTag | kExternalStringTag,
  LONG_PRIVATE_EXTERNAL_ASCII_STRING_TYPE = LONG_EXTERNAL_ASCII_STRING_TYPE,
  /*
    下面是所有类型都不属于字符串类型,kNotStringTag是128,即10000000,
    因为最高一位是0表示是字符串类型,所以10000000保证了大于所有字符串类型的值
  */
  MAP_TYPE = kNotStringTag,
  HEAP_NUMBER_TYPE,
  FIXED_ARRAY_TYPE,
  CODE_TYPE,
  ODDBALL_TYPE,
  PROXY_TYPE,
  BYTE_ARRAY_TYPE,
  FILLER_TYPE,
  SMI_TYPE,

  ACCESSOR_INFO_TYPE,
  ACCESS_CHECK_INFO_TYPE,
  INTERCEPTOR_INFO_TYPE,
  SHARED_FUNCTION_INFO_TYPE,
  CALL_HANDLER_INFO_TYPE,
  FUNCTION_TEMPLATE_INFO_TYPE,
  OBJECT_TEMPLATE_INFO_TYPE,
  SIGNATURE_INFO_TYPE,
  TYPE_SWITCH_INFO_TYPE,
  DEBUG_INFO_TYPE,
  BREAK_POINT_INFO_TYPE,
  SCRIPT_TYPE,

  JS_OBJECT_TYPE,
  JS_GLOBAL_OBJECT_TYPE,
  JS_BUILTINS_OBJECT_TYPE,
  JS_VALUE_TYPE,
  JS_ARRAY_TYPE,

  JS_FUNCTION_TYPE,

  // Pseudo-types
  FIRST_NONSTRING_TYPE = MAP_TYPE,
  FIRST_TYPE = 0x0,
  LAST_TYPE = JS_FUNCTION_TYPE,
  
  FIRST_JS_OBJECT_TYPE = JS_OBJECT_TYPE,
  LAST_JS_OBJECT_TYPE = JS_ARRAY_TYPE
}

示例图如下
在这里插入图片描述
我们对c++对象的类型大概有了一个印象,下面看一下Object类的定义。下面是几个宏,用来判断c++对象的类型的。

#define HAS_SMI_TAG(value) ((reinterpret_cast<int>(value) & kSmiTagMask) == kSmiTag)

#define HAS_FAILURE_TAG(value) ((reinterpret_cast<int>(value) & kFailureTagMask) == kFailureTag)

#define HAS_HEAP_OBJECT_TAG(value) ((reinterpret_cast<int>(value) & kHeapObjectTagMask) == kHeapObjectTag

下面是一些有代表性的isType函数的定义。

// 地址的低位是否是0
bool Object::IObject::isSmi() {
  return HAS_SMI_TAG(this);
}

// 低两位是01
bool Object::IsHeapObject() {
  return HAS_HEAP_OBJECT_TAG(this);
}

// 类型判断,在map里标记
bool Object::IsHeapNumber() {
  return Object::IsHeapObject()
    && HeapObject::cast(this)->map()->instance_type() == HEAP_NUMBER_TYPE;
}

bool Object::IsString() {
  return Object::IsHeapObject()
    && HeapObject::cast(this)->map()->instance_type() < FIRST_NONSTRING_TYPE;
}

bool Object::IsSeqString() {
  return IsString()
    && (String::cast(this)->representation_tag() == kSeqStringTag);
}

bool Object::IsByteArray() {
  return Object::IsHeapObject()
    && HeapObject::cast(this)->map()->instance_type() == BYTE_ARRAY_TYPE;
}

bool Object::IsJSObject() {
  return IsHeapObject()
    && HeapObject::cast(this)->map()->instance_type() >= JS_OBJECT_TYPE;
}
// 根据单例map对象判断类型
bool Object::IsContext() {
  return Object::IsHeapObject()
    && (HeapObject::cast(this)->map() == Heap::context_map() ||
        HeapObject::cast(this)->map() == Heap::global_context_map());
}

bool Object::IsUndefined() {
  return this == Heap::undefined_value();
}


bool Object::IsTheHole() {
  return this == Heap::the_hole_value();
}

下面继续看其他函数的定义。

1 解包对象里的数字。smi是小整形,在v8中表示整形。长度是31位。

double Object::Number() {
  return IsSmi()
    ? static_cast<double>(reinterpret_cast<Smi*>(this)->value())
    : reinterpret_cast<HeapNumber*>(this)->value();
}

2 转成smi对象

Object* Object::ToSmi() {
  // 已经是,直接返回
  if (IsSmi()) return this;
  // 是堆对象
  if (IsHeapNumber()) {
    double value = HeapNumber::cast(this)->value();
    // double to int类型
    int int_value = FastD2I(value);
    if (value == FastI2D(int_value) && Smi::IsValid(int_value)) {
      return Smi::FromInt(int_value);
    }
  }
  return Failure::Exception();
}

3 判断index是不是字符串的有效长度

bool Object::IsStringObjectWithCharacterAt(uint32_t index) {
  if (!this->IsJSValue()) return false;

  JSValue* js_value = JSValue::cast(this);
  if (!js_value->value()->IsString()) return false;

  String* str = String::cast(js_value->value());
  if (index >= (uint32_t)str->length()) return false;

  return true;
}

4 参考js的IsInstanceOf。很多类型后面的时候分析。

bool Object::IsInstanceOf(FunctionTemplateInfo* expected) {
  // js对象的基类
  if (!this->IsJSObject()) return false;
  
  Object* cons_obj = JSObject::cast(this)->map()->constructor();
  if (!cons_obj->IsJSFunction()) return false;
  JSFunction* fun = JSFunction::cast(cons_obj);
  
  for (Object* type = fun->shared()->function_data();type->IsFunctionTemplateInfo(); type = FunctionTemplateInfo::cast(type)->parent_template()) {
    if (type == expected) return true;
  }
  // Didn't find the required type in the inheritance chain.
  return false;
}

5 ToObject,其他类型转成对象类型。js的原生类型需要转成对象的时候。具体的在分析子类时再详细分析。

static Object* CreateJSValue(JSFunction* constructor, Object* value) {
  // 分类一个以constructor为构造函数的对象
  Object* result = Heap::AllocateJSObject(constructor);
  if (result->IsFailure()) return result;
  JSValue::cast(result)->set_value(value);
  return result;
}


Object* Object::ToObject(Context* global_context) {
  if (IsNumber()) {
    return CreateJSValue(global_context->number_function(), this);
  } else if (IsBoolean()) {
    return CreateJSValue(global_context->boolean_function(), this);
  } else if (IsString()) {
    return CreateJSValue(global_context->string_function(), this);
  }
  ASSERT(IsJSObject());
  return this;
}


Object* Object::ToObject() {
  Context* global_context = Top::context()->global_context();
  if (IsJSObject()) {
    return this;
  } else if (IsNumber()) {
    return CreateJSValue(global_context->number_function(), this);
  } else if (IsBoolean()) {
    return CreateJSValue(global_context->boolean_function(), this);
  } else if (IsString()) {
    return CreateJSValue(global_context->string_function(), this);
  }

  // Throw a type error.
  return Failure::InternalError();
}

6 ToBoolean,判断js变量是true或false的时候使用。

Object* Object::ToBoolean() {
  if (IsTrue()) return Heap::true_value();
  if (IsFalse()) return Heap::false_value();
  // 数字0是false
  if (IsSmi()) {
    return Heap::ToBoolean(Smi::cast(this)->value() != 0);
  }
  // null或undefined是false
  if (IsUndefined() || IsNull()) return Heap::false_value();
  // Undetectable object is false
  if (IsUndetectableObject()) {
    return Heap::false_value();
  }
  // 空字符串是false
  if (IsString()) {
    return Heap::ToBoolean(String::cast(this)->length() != 0);
  }
  // 
  if (IsHeapNumber()) {
    return HeapNumber::cast(this)->HeapNumberToBoolean();
  }
  return Heap::true_value();
}

7 属性查找,使用具体类型的查找函数

void Object::Lookup(String* name, LookupResult* result) {
  if (IsJSObject()) return JSObject::cast(this)->Lookup(name, result);
  Object* holder = NULL;
  Context* global_context = Top::context()->global_context();
  if (IsString()) {
    holder = global_context->string_function()->instance_prototype();
  } else if (IsNumber()) {
    holder = global_context->number_function()->instance_prototype();
  } else if (IsBoolean()) {
    holder = global_context->boolean_function()->instance_prototype();
  }
  ASSERT(holder != NULL);  // cannot handle null or undefined.
  JSObject::cast(holder)->Lookup(name, result);
}

8 查找原型对象

Object* Object::GetPrototype() {
  // 对象的原型对象存在map对象里
  if (IsJSObject()) return JSObject::cast(this)->map()->prototype();
  Context* context = Top::context()->global_context();

  if (IsNumber()) return context->number_function()->instance_prototype();
  if (IsString()) return context->string_function()->instance_prototype();
  if (IsBoolean()) {
    return context->boolean_function()->instance_prototype();
  } else {
    return Heap::null_value();
  }
}

还有几个属性查找的函数,依赖一些子类,分析完子类再分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要下载elasticsearchhead0.1.5谷歌插件,您可以按照以下步骤进行操作: 1. 首先,打开您的Chrome浏览器,并在地址栏中输入"Chrome网上应用店"。 2. 在搜索栏中,输入"elasticsearchhead0.1.5"并点击搜索按钮。 3. 在搜索结果中,应该能找到相关的插件。请确保选择的插件是"elasticsearchhead0.1.5"。 4. 点击插件的名称或图标,进入插件的详情页面。 5. 在详情页面,您将看到一个"添加至Chrome"的按钮。点击该按钮以安装插件。 6. 安装完成后,您将收到一个确认消息,并且插件的图标将出现在Chrome浏览器的工具栏或扩展程序区域。 7. 现在,您已成功下载并安装了elasticsearchhead0.1.5谷歌插件。 请注意,插件的下载和安装过程可能因不同的Chrome浏览器版本而有所不同。如果您遇到任何问题,请参考官方文档或在相关论坛寻求帮助。 ### 回答2: Elasticsearch Head是一个用于查看和管理Elasticsearch集群的插件。虽然插件名称中包含谷歌,但实际上Elasticsearch Head插件不是谷歌官方出品的插件,它是开源社区开发的一个插件。要下载Elasticsearch Head插件,可以按照以下步骤进行操作: 1. 打开谷歌浏览器,进入插件商店。 2. 在插件商店搜索框中输入“Elasticsearch Head”并回车。 3. 在搜索结果中选择合适的插件,通常是由mobz开发的插件。 4. 点击插件卡片上的“添加至Chrome”按钮,开始下载和安装插件。 5. 下载完成后,浏览器右上角会出现一个新的插件图标。 6. 点击插件图标,弹出一个新的页面,输入Elasticsearch集群的地址和端口号。 7. 输入正确的地址和端口号后,点击连接按钮,即可访问和管理Elasticsearch集群。 需要注意的是,Elasticsearch Head插件只适用于谷歌浏览器,其他浏览器可能无法使用该插件。此外,插件下载和使用需要一定的网络条件和相关知识,对于不熟悉的用户,可以参考相关教程或向社区寻求帮助。 ### 回答3: 要下载elasticsearch-head 0.1.5插件,可以按照以下步骤进行操作: 1. 打开谷歌浏览器,并在地址栏中输入“elasticsearch-head 0.1.5谷歌插件下载”。 2. 在搜索结果页面,可以找到elasticsearch-head插件的官方下载页面或第三方站点提供的下载链接。 3. 点击相关链接,进入插件下载页面。 4. 在插件下载页面,可以选择合适的插件版本,确认选择0.1.5版本。 5. 阅读下载页面上的相关信息,了解插件的功能和适用版本。 6. 确认无误后,点击下载按钮开始下载插件。 7. 下载完成后,根据提示,将下载的插件文件保存到本地计算机的特定文件夹中。 8. 在谷歌浏览器中,点击浏览器右上方三个点的菜单按钮,选择“更多工具”->“扩展程序”。 9. 在打开的扩展程序页面上,找到“elasticsearch-head”插件,并启用它。 10. 完成上述操作后,elasticsearch-head插件将被成功安装,并在浏览器的工具栏上显示图标,点击图标即可使用该插件进行Elasticsearch相关操作。 请注意,在下载和安装插件的过程中,要确保从官方或可信赖的第三方网站下载插件,以避免安全隐患。另外,确认插件的兼容性和适用版本,以确保它能够正常运行在您的谷歌浏览器上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值