JS核心理论之《数据类型、类型转换、深浅拷贝与参数传递》

273 阅读11分钟

数据类型

基本类型:共7种,也被称为值类型,是一种既非对象也无方法的数据。包括:string、number、bigint、boolean、null、undefined、symbol。

除了 null 和 undefined之外,所有基本类型都有其对应的包装对象: String 为字符串基本类型。 Number 为数值基本类型。 BigInt 为大整数基本类型。 Boolean 为布尔基本类型。 Symbol 为字面量基本类型。 这个包裹对象的valueOf()方法返回基本类型值。

引用类型:对象(Object)、数组(Array)、函数(Function)、Date、RegExp、基本包装类型(String、Number、Boolean、BigInt、Symbol)以及单体内置对象(Global、Math)

二者区别

  • 基本类型
    • 基本类型的值是不可改变的。
    • 基本类型的值保存在栈中。
    • 基本类型的比较是值的比较。
    • 保存与复制的是值本身。
    • 使用typeof检测数据的类型。
  • 引用类型
    • 引用类型可以拥有属性和方法,并且是可以动态改变的.
    • 引用类型的值是同时保存在栈内存和堆内存中的对象。
    • 引用类型的比较是地址的比较。
    • 保存与复制的是指向对象的一个指针。
    • 使用instanceof检测数据类型。

对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体里属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true. 如果是不同的对象,即使包含相同的属性和值,也会返回false 示例:

var arrRef = ["Hi!"];
var arrRef2 = arrRef;
console.log(arrRef === arrRef2); // true

var arr1 = ["Hi!"];
var arr2 = ["Hi!"];
console.log(arr1 === arr2); // false

示例:

var a,b;
a = "zyj";
b = a;

a.toUpperCase();
console.log(a);   // zyj
console.log(b);   // zyj

a = "呵呵";       // 改变 a 的值,并不影响 b 的值
console.log(a);   // 呵呵
console.log(b);   // zyj

image

示例:

var a = {name:"percy"};
var b;
b = a;
a.name = "zyj";
console.log(b.name);    // zyj

b.age = 22;
console.log(a.age);     // 22
var c = {
  name: "zyj",
  age: 22
};

image

类型转换

显式调用Boolean(value)、Number(value)、String(value)完成的类型转换,叫做显示类型转换。

比较操作或者加减乘除四则运算操作时,常常会触发 JavaScript 的隐式类型转换。隐式类型转换时,绝大多数情况下都是优先转为number型

js内部用于实现类型转换的4个函数是:

  • ToPrimitive ( input [ , PreferredType ] ), 转换为原始对象,即基本类型,依赖于valueOf和toString的实现(先valueOf,再toString)
  • ToBoolean ( argument ),除了以下(undefined、null、-0/+0、NaN、'')五个值的转换结果为false,其他的值全部为true
  • ToNumber ( argument )
  • ToString ( argument )

比较运算

JavaScript 为我们提供了严格比较与类型转换比较两种模式,严格比较(===)只会在操作符两侧的操作对象类型一致,并且内容一致时才会返回为 true,否则返回 false。 而更为广泛使用的 == 操作符则会首先将操作对象转化为相同类型,再进行比较。对于 <= 等运算,则会首先转化为原始对象(Primitives),然后再进行对比。

x == y, 算法流程如下:

  1. x或y中有一个为NaN, 则返回false;
  2. x或y都为null或undefined中的一种,则返回true(null == undefined),否则返回false(null == 0);
  3. x或y类型不一致,且为String,Number,Boolean中的一种,则x、y转为Number再比较;
  4. x或y中有一个为Object,则将其转为原始类型,再进行比较;
优先比类型,再比null与undefined,再比string和number,再比boolean与any,再比object与string、number、symbol;
以上如果转为原始类型比较,则进行类型转换,直到类型相同再比较值的大小。这就是==的隐式转换对比

image

示例:

[] == ![] //true
1. 基于运算符的优先级,运算![],[]转为boolean后为真值,取反后,变为:[]==false
2. 任何类型与boolean比较,boolean转为number,即 []==0
3. []为对象,转为原始值,ToPrimitive先valueOf返回[],再toString返回''
4. 最后string转number,变为 0==0

加法运算

遇到算数运算符(- 、* 、/ 和 %)的时候会在运算之前将参与运算的双方转换成数字。而加法(+)运算有些特殊,只要其中一个操作数是字符串,那么它就执行连接字符串的操作。

加法(+)的算法如下:

  1. +号左右分别进行取值,进行ToPrimitive()操作,转为原始值;
  2. 分别获取左右转换之后的值,如果存在String,则对其进行ToString处理后进行拼接操作;
  3. 其他的都进行ToNumber处理;
  4. 在转换时ToPrimitive,除去Date为string外,都按照ToPrimitive 类型为Number进行处理;

示例:

1+'2'+false

1.左边取原始值,依旧是Number
2.中间为String,则都进行toString操作
3.左边转换按照toString的规则,返回'1',得到结果temp值'12'
4.右边布尔值和temp同样进行1步骤
5.temp为string,则布尔值也转为string'false'
6.拼接两者 得到最后结果 '12false'

对象转换

只有在 JavaScript 表达式或语句中需要用到数字或字符串时,对象才被隐式转换。 当需要将对象转换成数字时,需要以下三个步骤:

  1. 调用 valueOf()。如果结果是原始值(不是一个对象),则将其转换为一个数字。
  2. 否则,调用 toString() 方法。如果结果是原始值,则将其转换为一个数字。
  3. 否则,抛出一个类型错误。

示例:

> 3 * { valueOf: function () { return 5 } }
15

类型判断

  1. 判断数组
  • 使用Array.isArray()判断数组
  • 使用[] instanceof Array判断是否在Array的原型链上,即可判断是否为数组
  • [].constructor === Array通过其构造函数判断是否为数组
  • 也可使用Object.prototype.toString.call([])判断值是否为[object Array]来判断数组
  1. 判断对象
  • Object.prototype.toString.call({})结果为[object Object]则为对象
  • {} instanceof Object判断是否在Object的原型链上,即可判断是否为对象
  • {}.constructor === Object通过其构造函数判断是否为对象
  1. 判断函数
  • 使用typeof function判断func是否为函数
  • 使用func instanceof Function判断func是否为函数
  • 通过func.constructor === Function判断是否为函数
  • 也可使用Object.prototype.toString.call(func)判断值是否为[object Function]来判断func
  1. 判断null
  • 最简单的是通过null===null来判断是否为null
  • Object.prototype.__proto__===a判断a是否为原始对象原型的原型,即null
  • typeof (a) == 'object' && !a 通过typeof判断null为对象,且对象类型只有null转换为Boolean为false
  1. 判断NaN
  • isNaN(any)直接调用此方法判断是否为非数值

深浅拷贝

  • 浅拷贝: 拷贝的是对象的指针,修改内容互相影响
  • 深拷贝:整个对象拷贝到另一块内存空间中,修改内容不互相影响
名词 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变
如下例子:对对象直接复制后,导致原对象值发生改变
let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

解决办法一:
let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

解决办法二:
let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

解决办法三:
let a = ['ant', 'bison', 'camel', 'duck', 'elephant']
let b = a.slice(1, 5)
console.log(b) // ["bison", "camel", "duck", "elephant"]

浅拷贝只解决了第一层的问题,但是如果遇到嵌套对象,就不行了,就得用深拷贝。

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

解决办法一:

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是JSON.parse(JSON.stringify(object))方法也是有局限性的:

  1. 会忽略undefined和symbol
  2. 不能序列化函数
  3. 不能解决循环引用的对象

解决办法二:自己实现深拷贝函数(考虑了对象、数组、Symbol类型以及多层嵌套)

function deepClone(obj) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非对象')
  }

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [] : {}
 
  Reflect.ownKeys(obj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj
}


const e = Symbol("e")
const f = Symbol.for("f")
let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
obj[e] = 'localSymbol'
obj[f] = 'globalSymbol'


let newObj = deepClone(obj)
newObj.b.c = 1

console.log(newObj)  // { a: [ 1, 2, 3 ], b: { c: 2, d: 3 }, [Symbol(e)]: 'localSymbol', [Symbol(f)]: 'globalSymbol' }
console.log(newObj[e] === obj[e]) // true
console.log(obj.b.c) // 2

上述函数的问题是没有考虑循环引用以及来自原型链上的属性的拷贝。

let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
obj.e = obj

let newObj = deepClone(obj)
console.log(newObj.e) // 2

>输出
RangeError: Maximum call stack size exceeded
let childObj = Object.create(obj)
let newObj = deepClone(childObj)

console.log('原对象:')
for(let key in childObj){
  console.log(childObj[key])
}
console.log('新对象:')
for(let key in newObj){
  console.log(newObj[key])
}

>输出
原对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
新对象:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系。 当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这里使用Reflect.ownKeys() 获取所有的键值,同时包括 Symbol

  • for...in 获取当前对象及其原型链上的所有可枚举属性
  • Object.keys 获取当前对象上的所有可枚举属性
  • Object.getOwnPropertyNames 获取当前对象上的所有可枚举和不可枚举属性
  • Object.getOwnPropertySymbols 获取当前对象上所有Symbol属性
  • Reflect.ownKeys 获取当前对象上所有可枚举、不可枚举属性以及Symbol属性
  • Object.getPrototypeOf 获取对象原型链上一级的对象
  • Reflect.getPrototypeOf 获取对象原型链上一级的对象

所有通过Object和Reflect方法获取对象的属性,都无法访问到对象原型链上的属性

  • Object.keys 是获取到对象属性的所有方法中范围最小的一种方法
  • Reflect.ownKeys 是获取到对象属性的所有方法中范围最大的一种方法
  • 此外 Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols
function deepClone(obj, wm = new WeakMap()) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非对象')
  }

  if (wm.has(obj)) return wm.get(obj); // 新增代码,查哈希表

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [] : {}
  wm.set(obj, newObj); // 新增代码,哈希表设值
  
  Object.getOwnPropertySymbols(obj).forEach(symKey => {
    newObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey], wm) : obj[symKey]
  })

  //使用for in替换Reflect.ownKeys
  for( let key in obj){
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key],wm) : obj[key]
  }

  return newObj
}

测试一下:

const e = Symbol('e')
const f = Symbol.for('f')
const g = Symbol.for('g')
let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3,
  },
}
obj[e] = 'localSymbol'
obj[f] = 'globalSymbol'

let childObj = Object.create(obj)
childObj[g] = 'globalSymbol_'

let newObj = deepClone(childObj)


console.log('原对象:')
for (let key in childObj) {
  console.log(childObj[key])
}
while (childObj) { // 循环
  Object.getOwnPropertySymbols(childObj).forEach(symKey => {
    console.log(childObj[symKey])
  })
  childObj = Object.getPrototypeOf(Object(childObj))
}

console.log('新对象:')
for (let key in newObj) {
  console.log(newObj[key])
}
while (newObj) { // 循环
  Object.getOwnPropertySymbols(newObj).forEach(symKey => {
    console.log(newObj[symKey])
  })
  newObj = Object.getPrototypeOf(Object(newObj))
}


>输出
原对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
globalSymbol_
localSymbol
globalSymbol
新对象:
[ 1, 2, 3 ]
{ c: 2, d: 3 }
globalSymbol_

此时,上述函数可以深拷贝当前对象或数组的所有可枚举属性、Symbol类型键,以及该对象原型链上的所有可枚举属性。但仍然有一个问题,就是不能拷贝原型链上的Symbol类型键。 需要使用Object.getPrototypeOf来循环获取上一级对象的Symbol类型键属性。

function deepClone(obj, wm = new WeakMap()) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null
  }

  if (!isObject(obj)) {
    throw new Error('非对象')
  }

  if (wm.has(obj)) return wm.get(obj) // 新增代码,查哈希表

  let isArray = Array.isArray(obj)
  let newObj = isArray ? [] : {}
  wm.set(obj, newObj) // 新增代码,哈希表设值

  while (obj) {
    Reflect.ownKeys(obj).forEach(key => {
      if(obj.propertyIsEnumerable(key)){
        newObj[key] = isObject(obj[key]) ? deepClone(obj[key],wm) : obj[key]
      }
    })
    obj = Object.getPrototypeOf(Object(obj))
  }

  return newObj
}

至此,该函数可以深拷贝当前对象和它原型链上的所有可枚举属性及Symbol属性,结果大家可以去验证。

总结

浅拷贝

  • Object.assign()
  • 扩展运算符 ...
  • Array.prototype.slice()/Array.prototype.concat()

深拷贝

  • JSON.parse(JSON.stringify())
  • lodash的深拷贝函数

参数传递

  • 基本类型传值调用(值拷贝)
  • 引用类型传共享调用(指针拷贝)

1.关键点是函数传参时,传入的是实参的拷贝,而不是实参本身。所以,基本类型传递的是变量的值的拷贝,而引用类型传递对象的指针的拷贝,其指针也是变量的值。 所以传共享调用也可以说是传值调用。

2.值拷贝后,对值修改,自然不会影响原值。指针拷贝后,与原指针指向的是同一个对象,如果函数内修改对象的属性,刚原对象属性自然也变,但如果直接对指针拷贝赋予新值,即修改它的指向,则不会影响到原指针指向的原对象。

示例:

function changeStuff(a, b, c) {
  a = a * 10;
  b.item = "changed";
  c = {item: "changed"};
}

var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(num, obj1, obj2);

console.log(num); // 10
console.log(obj1.item);  //changed
console.log(obj2.item);  //unchanged

image

可以看到,变量 a 的值就是 num 值的拷贝,变量 b c 分别是 obj1 obj2 的指针的拷贝。 函数的参数其实就是函数作用域内部的变量,函数执行完之后就会销毁。

image

变量 a 的值的改变,并不会影响变量 num。 而 b 因为和 obj1 是指向同一个对象,所以使用 b.item = "changed"; 修改对象的值,会造成 obj1 的值也随之改变。 由于是对 c 重新赋值了,所以修改 c 的对象的值,并不会影响到 obj2。