普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月26日首页

别再混淆了!JS类型转换底层:valueOf vs toString vs Symbol.toPrimitive 详解

作者 cmd
2026年3月26日 14:23

js获取对象值的方法有三种valueOf()toString()symbol.toPrimitive这些其实是类型转换的问题;三种方式本质上略微不同;

我们知道在js中,'一切皆为对象'。每个对象都有一个toString()方法和valueOf方法,其中toString()方法返回一个表示该对象的字符串,valueOf 方法返回该对象的原始值。

一、valueOf() 与 toString()

基本类型的情况下:

const str = "hello",n = 123,bool = true;
console.log(typeof(str.toString()) + "_" + str.toString())        //string_hello
console.log(typeof(n.toString()) + "_" + n.toString()  )            //string_123
console.log(typeof(bool.toString()) + "_" + bool.toString())        //string_true

console.log(typeof(str.valueOf()) + "_" + str.valueOf())            //string_hello
console.log(typeof(n.valueOf()) + "_" + n.valueOf())                //number_123
console.log(typeof(bool.valueOf()) + "_" + bool.valueOf())          //boolean_true

// valueOf
console.log(str.valueOf() === str)  // true
console.log(n.valueOf() === n) // true
console.log(bool.valueOf() === bool) // true
// toString
console.log(str.toString() === str) // true
console.log(n.toString() === n)     // false
console.log(bool.toString() === bool) // false

toString 方法对于值类型数据使用而言,其效果相当于类型转换,将原类型转为字符串。

valueOf 方法对于值类型数据使用而言,其效果将相当于返回原数据。 引用类型的情况下:

var obj = {};

console.log(obj.toString());    //[object Object] 返回对象类型
console.log(obj.valueOf());     //{} 返回对象本身

综合例子:

let test = { 
    i: 10, 
    toString: function() {
       console.log('toString');
       return this.i; 
    }, 
    valueOf: function() { 
       console.log('valueOf');
       return this.i; 
    }
} 
console.log(test);          // { I:10, toString: f, valueOf: f }
console.log(+test);         // 10 valueOf
console.log('' + test);       // 10 valueOf
console.log(String(test));  // 10 toString
console.log(Number(test));  // 10 valueOf
console.log(test == '10');  // true valueOf
console.log(test == '10');  // true valueOf
console.log(test === '10'); // false

个人理解:

带有运算符的获取值的方式都会走valueOf()方法;强转字符串的时候走toString()方法;

二、toString() 和 String()

  • toString()
    • toString()可以将所有的数据都转换为字符串,但是要排除nullundefined
    • nullundefined不能转换为字符串,nullundefined调用toString()方法会报错
    • 如果当前数据为数字类型,则toString()括号中的可以写一个数字,代表进制,可以将数字转化为对应进制字符串。
var num = 123;
console.log(num.toString()+'_'+ typeof(num.toString()));    //123_string
console.log(num.toString(2)+'_'+typeof(num.toString()));    //1111011_string
console.log(num.toString(8)+'_'+typeof(num.toString()));    //173_string
console.log(num.toString(16)+'_'+typeof(num.toString()));   //7b_string
  • String()
    String()可以将nullundefined转换为字符串,但是没法转进制字符串。

三、Symbol.toPrimitive

对象的Symbol.toPrimitive属性。指向一个方法。该对象被转化为原始类型的值时,会调用这个办法,返回该对象对应的原始类型值。 Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一个有三种模式。

  • Number: 该场合需要转成数值
  • String: 该场合需要转成字符串
  • Default: 该场合可以转成数值,也可以转成字符串。

Symbol.toPrimitive在类型转换方面,优先级是最高的

const test = { 
i: 10, 
toString: function() {
   console.log('toString');
  return this.i; 
}, 
valueOf: function() { 
   console.log('valueOf');
   return this.i; 
},
    [Symbol.toPrimitive](hint) {
        if(hint === 'number'){
          console.log('Number场景');
          return 123;
        }
        if(hint === 'string'){
          console.log('String场景');
          return 'str';
        }
        if(hint === 'default'){
          console.log('Default 场景');
          return 'default';
        }
    }
}

console.log(test);          // { i:10, toString: f, valueOf: f, Symbol(Symbol.toPrimitive): f }
console.log(+test);         // 123 Number场景
console.log(''+test);       // default Default 场景
console.log(String(test));  // str String场景
console.log(Number(test));  // 123 Number场景
console.log(test == '10');  // false default场景
console.log(test === '10'); // false

上面代码中、+test中的加号命名为一元加号+test本质就是转成数值的意思;

Tips

console.log(3 + test);  // 3default Default 场景
console.log(3 - test);  // -120 Number场景
console.log(3 * test);  // 369 Number场景
console.log(3 / test);  // 0.0243902 Number场景

以上的代码中,加减乘除都算运算符,本应都应该走Number场景,但是唯独+号走了Default场景

四、一元加号

一元加号运算符 + 在其操作数之前,并计算其操作数;但如果尚未将其转换为数字,则尝试将其转换为数字

console.log(+'')  // 0
console.log(+true)  // 1
console.log(+false)  // 0
console.log(+'hello')  // NaN

console.log(1 + +"2" + "2")  // 32

一元加法是将某事物转换为数字的最快和首选方法,因为它不对数字执行任何其他操作。

如果它无法解析特定值,它将输出为NaN

感谢您抽出宝贵的时间观看本文;本文是JavaScript系列的第 6 篇,后续会持续更新,欢迎关注~

昨天 — 2026年3月25日首页

JS深浅拷贝全解析|常用方法+手写实现+避坑指南(附完整代码)

作者 cmd
2026年3月25日 09:05

一、拷贝

  • 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后互相影响的问题。
  • 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址,原对象和拷贝对象还是会互相影响。
  • 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和拷贝对象互相影响。

二、浅拷贝

可实现浅拷贝的方式如下:

1. Object.assign

const obj = {
  name: 'lin'
}
const newObj = Object.assign({}, obj)
obj.name = 'xxx' // 改变原来的对象
console.log(newObj) // { name: 'lin' } 新对象不变
console.log(obj == newObj) // false 两者指向不同地址

2. 数组的slice和concat方法

const newArr = arr.slice(0)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome']

console.log(arr == newArr) // false 两者指向不同地址
const arr = ['lin', 'is', 'handsome']
const newArr = [].concat(arr)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变

console.log(arr == newArr) // false 两者指向不同地址

3. 数组的静态方法 Array.from

const arr = ['lin', 'is', 'handsome']
const newArr = Array.from(arr)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome']

console.log(arr == newArr) // false 两者指向不同地址

4.扩展运算符

const arr = ['lin', 'is', 'handsome']
const newArr = [...arr]

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变

console.log(arr == newArr) // false 两者指向不同地址
const obj = {
  name: 'lin'
}

const newObj = { ...obj }

obj.name = 'xxx' // 改变原来的对象

console.log(newObj) // { name: 'lin' } // 新对象不变

console.log(obj == newObj) // false 两者指向不同地址

5. 循环遍历赋值

function clone (obj) {
  const cloneObj = {} // 创建一个新的对象
  for (const key in obj) { // 遍历需克隆的对象
    cloneObj[key] = obj[key] // 将需要克隆对象的属性依次添加到新对象上
  }
  return cloneObj
}

三、深拷贝

1. 序列化

JSON.parse(JSON.stringify(obj))

const obj = {
  person: {
    name: 'lin'
  }
}

const newObj = JSON.parse(JSON.stringify(obj))
obj.person.name = 'xxx' // 改变原来的深层对象

console.log(newObj) // { person: { name: 'lin' } } 新的深层对象不变

使用序列化的方式来实现深度克隆有些许弊端;

  • 会忽略undefinedsymbol函数
  • NaN Infinity -Infinity会被序列化为null
  • Map序列化返回是空对象{}; 个人认为Map的结构类似键值对,然后value是个函数,因函数的缘故无法序列化

2. 递归实现

要求:

  • 支持对象、数组、日期、正则的拷贝。
  • 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
  • 处理 Symbol 作为键名的情况。
  • 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,问题不大)。
  • 处理 DOM 元素(DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个)。
  • 额外开辟一个储存空间 WeakMap,解决循环引用递归爆栈问题(引入 WeakMap 的另一个意义,配合垃圾回收机制,防止内存泄漏)。

Reflect.ownKeys() 方法返回一个由目标对象(自身)的属性键组成的数组(包括Symbol);它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1)
}
function deepClone(target, hash = new WeakMap()) {
  // 处理 原始值 null、undefined、number、string、symbol、bigInt、boolean
  if (typeof target !== 'object' || target === null) {
    return target
  }
  // 处理 array
  if (Array.isArray(target)) {
    return target.map((e) => deepClone(e))
  }
  // 处理 function
  if (getType(target) === 'Function') {
    return eval(`(${target.toString()})`).bind(this) // function 声明需要用"("、")"包裹
  }
  // 拷贝日期 
  if(getType(target) === 'Date') {
    return new Date(target.valueOf()) 
 }
  // 拷贝正则
  if(getType(target) === 'RegExp') {
    return new RegExp(target)
 }
  // 处理 map
  if (getType(target) === 'Map') {
    let map = new Map()
    target.forEach((v, k) => {
      map.set(k, deepClone(v))
    })
    return map
  }
  // 处理 set
  if (getType(target) === 'Set') {
    let set = new Set()
    for (let val of target.values()) {
      set.add(deepClone(val))
    }
    return set
  }
    
  if (hash.get(target) return hash.get(target) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
  const cloneTarget = new target.constructor() // 通过target的构造函数创建一个新的与之一样类型的对象,这样写的就不需要判断类型了
  hash.set(target, cloneTarget) // 如果存储空间中没有就存进存储空间 hash 里

  // 处理 object
  if (getType(target) === 'Object') {
Reflect.ownKeys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], hash) // 递归拷贝每一层
  })
    return cloneTarget
  }
  return target
}

3. structuredClone

结构化克隆算法是由HTML5规范定义的用于复制复杂JavaScript对象的算法;H5定义的全局深度克隆方法;

const obj = {
  person: {
    name: 'lin'
  }
}

const newObj = structuredClone(obj) // 
obj.person.name = 'xxx' // 改变原来的对象

console.log('原来的对象', obj)
console.log('新的对象', newObj)

console.log('更深层的对象指向不同的地址', obj.person == newObj.person)

缺点:

  • Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。

  • 对象的某些特定参数也不会被保留

    • RegExp对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。
  • Symbol除外可复制

最后说一局,推荐使用lodash,lodash的实现是很全面的

感谢您抽出宝贵的时间观看本文;本文是 JavaScript 系列的第 5 篇,后续会持续更新!欢迎关注~

昨天以前首页

吃透 ES6 Generator:yield/next/yield* 核心用法详解

作者 cmd
2026年3月24日 10:13

Generator 是 ES6 为异步编程和迭代器设计的核心特性,本文聚焦 Generator 函数的核心语法:yield 暂停执行、next () 传参机制、yield* 调用迭代器 / 生成器的逻辑,通过极简示例讲清原理;

ES6中也提出了新的循环方式for...of;它是只能用于可生成迭代对象的循环的;比如数组,Set, Map, String,自定义的迭代器等等;提这个知识点是它跟本文的知识点有关联;

一、Generator 定义

Generator主要用于异步编程,最大的特点就是交出函数的执行权(暂停);本质上,整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器;

返回方式为yieldyield命令是异步不同阶段的分界线

yieldreturn的区别:

  • yield:暂停函数,再次调用会执行到遇到下一个yield为止;
  • return:结束函数,再次调用会重新执行,直到return结束;

Generator 函数体内使用yield语句,可以定义不同的内部状态;

函数的调用方法:next()方法;会返回一个对象,{ value: 值,done:  布尔值 }

function* print(){
    yield 'a';
    yield 'b';
    yield 'c';
    return 'd ...end';
}
let p = print();
console.log(p.next())
// { value: 'a', done: false}
console.log(p.next())
// { value: 'b', done: false}
console.log(p.next())
// { value: 'c', done: false}
console.log(p.next())
// { value: 'd ...end', done: true}
console.log(p.next())
// { value: undefined, done: true}

当输出完成后done会变成true;接着调用的还是能返回值,但是返回的都是undefined

二、next()

Generator函数中的.next()方法可以接收参数;

  • 传入的参数,其实是把上一个yield语句的返回的值给覆盖;
  • 第一个.next()方法其实就是启动器,在它之前没有yield语句,所以给第一个.next()方法传参是没有意义的
function* generatorFunction() {
  const a = yield;
  while(true) {
    yield a;
  }
}

const generator = generatorFunction();

console.log(generator.next());  // { value: undefined, done: false}
console.log(generator.next(1)); // { value: 1, done: false }
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 1, done: false }

第一次调用的时候yield没有返回语句,所以是undefined;第二次调用的时候传1,接收到的是1,往后只会一直输出1

三、yield 和 yield*

比如创建一个生成器用于返回一个斐波那契数列,斐波那契数列定义如下:

  • 第一个数是 0
  • 第二个数是 1
  • 之后的每一个数是前两个数之和

换言之,即:F(0) = 0; F(1) = 1; ... F(n) = F(n-1) + F(n-2);

实际上我们希望在一个生成器里输出一些来自其它生成器的值。这时 yield* 就派上用场了:

function* fibonacciGeneratorFunction(a = 0, b = 1) {
  yield a;   
  yield* fibonacciGeneratorFunction(b, b + a);
}

const fibonacciGenerator = fibonacciGeneratorFunction();

fibonacciGenerator.next(); // { value: 0, done: false }
fibonacciGenerator.next(); // { value: 1, done: false }
fibonacciGenerator.next(); // { value: 1, done: false }
fibonacciGenerator.next(); // { value: 2, done: false }
fibonacciGenerator.next(); // { value: 3, done: false }
fibonacciGenerator.next(); // { value: 5, done: false }

yield* 是可用于调用其他的生成器的;

function* g1() {
  yield 2;
  yield 3;
  yield 4;
}

function* g2() {
  yield 1;
  yield* g1();
  yield 5;
}

const iterator = g2();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

yield* 的用处

yield* 紧跟的表达式可以是任何可生成迭代对象的迭代器或另一个生成器;可生成迭代对象的ObjectArray, String, Set, Map等;可以通过原型链上的Symbol.Iterator判断是否可生成迭代对象;

Symbol.IteratorSymbol的那一篇(第一篇)有提

四、作用

数组的合并分级遍历

let a = [1,2,3,4]
let b = [5,6,7,8]
// let c = [...a,...b]
// for(let key of c) {
//     console.log(key);
// }
function* obPrint(...args) {
    console.log(...args);
    for(const key of args) {
        yield* key
    }
}
for(const num of obPrint(a,b)) {
    console.log(num);
}

感谢您抽出宝贵的时间观看本文;本文是JavaScript系列的第4篇,后续会持续更新,欢迎关注~

❌
❌