变量
JavaScript变量是松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。
原始值&引用值
ECMAScript变量有原始值和引用值两种类型。原始值(primitive value)就是最简单的数据(如Number、String、Undefined等6种),引用值(reference value)则是由多个值构成的对象(如:Object、Array等)。
保存原始值的变量是按值访问的,我们所操作的就是存储在变量中实际的值。
let a = 10;
let b = a;
console.log(a,b);//10 10
b=30;
console.log(a,b);//10 30
复制代码
保存引用值的变量是按引用访问的。引用值是保存在内存中的对象。实际上操作的是对该对象的引用,而非实际的对象本身。我们可以看下面的例子,假设我们认为操作对象是操作其本身,那么我们可以理解为:系统复制一份一样的a的变量赋值给b,两个互不影响,这不难理解。但其实不是,a和b共享这个变量,它们引用同一块内存,可以理解为C语言中的指针。
let a = {};
let b = null;
b=a;
console.log(b===a)//true
复制代码
动态属性
对于引用值而言,我们可以动态的添加、修改或删除其属性,而原始值不能有属性,但是也有例外。
原始值不能有属性,若为其添加属性也不会报错,但是无效。
但是,原始类型的变量初始化有两种形式:字面量和new关键字,若采用new
关键字,则会创建一个Object
类型的实例,但其行为类似原始值,也就是说:功能上是原始值,本质是一个对象。
//原始值字面量
let name = 'zs';
name.age = 10;
console.log(name.age);//undefined
//原始值new关键字
let name = new String("zs");
console.log(name, typeof name);//[String: 'zs'] object
name.age = 18;
console.log(name);//[String: 'zs'] { age: 18 }
//引用值
let person = {};
person.name = 'wn';
person.age = 18;
console.log(person.name,person.age);//wn18
复制代码
复制值
对于原始值赋值到另一个变量时,原始值会被复制一份,这两个变量可以独立使用,互不干扰。
对于引用值,复制的实际上是一个指针,即两个变量指向同一个对象。
//原始值
let a = 10;
let b = a;
console.log(a,b);//10 10
b=30;
console.log(a,b);//10 30
//引用值
let zs = {name: 'zs'};
let person = zs;
console.log(person.name);//zs
zs.name='zss';
console.log(person.name);//zss
console.log(zs.name);//zss
复制代码
传递参数
ECMAScript中所有的函数参数都是按值传递的。函数外的值会被复制到函数内部的参数中。若是原始值,就跟原始值变量的复制一样,若是引用值,那么就跟引用值变量的复制一样,那这也意味着在函数中对对象的修改会反映到函数外部。
function setNum(num) {
num=20;
}
function setName(person) {
person.name='zss';
}
let num=10;
let zs = {name: 'zs'};
setName(zs);
setNum(num);
console.log(num);//10
console.log(zs);//{ name: 'zss' }
复制代码
引用传递参数时,在函数内部对对象属性的修改会反映到函数外部,因为它们指向的是同一个对象。但是函数参数是按值传递的,对像的传递可以理解为把指向当作值传入函数内部,我们不能改变引用。
注意:函数中参数就是局部变量,意味着函数执行结束便会被回收。
var person = {name: 'zs'};
function setName(person) {
person.name = 'zss';
console.log(person === window.person)//true
person = {name: 'ls'};
console.log(person === window.person)//false
console.log(person,window.person);//{name: "ls"} {name: "zss"}
}
setName(person);
console.log(person);//{name: "zss"}
复制代码
这个例子利用var
关键字在全局声明的属性会挂载到window
对象上这一特性,我们来比较函数参数和原始的对象有什么不同。由上例person===window.person
是false
可以看出,外部原始对象的引用并未改变,只是函数参数的指向变了。因为参数在函数内部也是局部变量,在函数执行完毕后销毁,所以函数内部的person
最后也就被回收了。下图反应了变量的指向改变过程一目了然。
确定类型
typeof
用来判断一个变量是否为undefined
、string
、number
和boolean
的最好方式。尤其注意typeof
对null
值的判断为object
。
typeof
对原始值很有用,但是对引用值用处不大,我们不关心它的类型是否为object
,而更想知道是什么类型的对象。instanceof
操作符的出现,实现了这个需求。
//typeof对原始值的判断
let n = 10, str = 'a', bool = false, undef;
console.log(typeof n);//number
console.log(typeof str);//string
console.log(typeof bool);//boolean
console.log(typeof undef);//undefined
console.log(typeof null);//object
//instanceof对引用值的判断
class Person {}
let zs=new Person();
let obj={};
console.log(zs instanceof Person);//true
console.log(Person[Symbol.hasInstance](zs));//true 这个写法与使用instanceof操作符效果一致
console.log(obj instanceof Person);//false
复制代码
深浅拷贝
深浅拷贝主要针对对象和数组。通俗来说,浅拷贝进行复制时,只遍历了一层,若这一层内有对象,则还会存在引用关系。深拷贝则递归一直到原始值,对原始值的复制,则是两个独立的、互不影响的值。我们常见的浅拷贝有:解构赋值(
...
展开运算符)、Object.assign()
等。
//解构赋值的浅拷贝
let obj1 = {a: 1, b: 2, arr: [1, 2, 3]};
let obj2 = {...obj1};
let obj3 = Object.assign(obj1, obj2);
obj2.a = 10;
obj2.arr[0] = 10;
console.log(obj1);//{ a: 1, b: 2, arr: [ 10, 2, 3 ] }
console.log(obj2);//{ a: 10, b: 2, arr: [ 10, 2, 3 ] }
console.log(obj3);//{ a: 1, b: 2, arr: [ 10, 2, 3 ] }
复制代码
通过上述代码我们不难理解浅拷贝的概念,第一层的属性则是独立的,而obj.arr
则是引用了同一个对象。
深拷贝的基本思路就是递归至原始值,再进行复制。JavaScript原生没有给我们提供深拷贝的功能函数,我们可以自己实现一个:
function deepClone(obj) {
let keys = null, newObj = null;
//对object和arr分别处理
if (Array.isArray(obj)) {
//生成数组的keys,即下标
keys = [...new Array(obj.length)].map((_, i) => i);
newObj = [];
} else {
//获取对象的key
keys = Object.getOwnPropertyNames(obj);
newObj = {};
}
for (let i = 0, len = keys.length; i < len; i++) {
let key = keys[i];
let val = obj[key];
//若不是原始值则递归
if (typeof val === "object")
newObj[key] = deepClone(val);
else newObj[key] = val;
}
return newObj;
}
let obj1 = {
a: 1,
b: 2,
c: {
d: 3,
e: 4,
arr: [1, 2, [3, 4]]
}
}
let obj2 = deepClone(obj1);
obj2.a = 10;
obj2.c.d = 30;
obj2.c.arr[0] = 10;
obj2.c.arr[2][0] = 30;
console.log(obj1);
//{ a: 1, b: 2, c: { d: 3, e: 4, arr: [ 1, 2, [ 3, 4 ] ] } }
console.log(obj2);
//{ a: 10, b: 2, c: { d: 30, e: 4, arr: [ 10, 2, [ 30, 4 ] ] } }
复制代码
我们可以看出,deepClone()
函数已经能够满足我们的需求了,但是我们会遇到一个新的问题,假如我们的对象是自定义对象呢?如下代码所示:
class Person {
constructor(name = '') {
this.name = name;
}
setName(name) {
this.name = name;
}
}
let obj1 = {
a: 1,
b: 2,
person: new Person('zs')
}
let obj2 = deepClone(obj1);
console.log(obj1);//{ a: 1, b: 2, person: Person { name: 'zs' } }
console.log(obj1.person.setName)//[Function: setName]
console.log(obj2);//{ a: 1, b: 2, person: { name: 'zs' } }
console.log(obj2.person.setName)//undefined
复制代码
从结果看出,我们的自定义Person
类包含的方法并没有被复制,原因是Object.getOwnPropertyNames(obj)
并没有获取到类的方法,我们可以这样解决:
//对object和arr分别处理
if (Array.isArray(obj)) {
//生成数组的keys,即下标
keys = [...new Array(obj.length)].map((_, i) => i);
newObj = [];
} else {
//获取对象的key
keys = Object.getOwnPropertyNames(obj);
//维持原有原型链
newObj = new obj.constructor();//通过实例化一个新的对象,来保留对象的原型链
}
复制代码
下面是完整的函数以及测试数据:
function deepClone(obj) {
//特殊处理
if (obj === null) return null
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
//对object和arr分别处理
let keys = null, newObj = null;
if (Array.isArray(obj)) {
//生成数组的keys,即下标
keys = [...new Array(obj.length)].map((_, i) => i);
newObj = [];
} else {
//获取对象的key
keys = Object.getOwnPropertyNames(obj);
//维持原有原型链
newObj = new obj.constructor();
}
for (let i = 0, len = keys.length; i < len; i++) {
let key = keys[i];
let val = obj[key];
//若不是原始值则递归
if (typeof val === "object")
newObj[key] = deepClone(val);
else newObj[key] = val;
}
return newObj;
}
class A {
constructor(val = null) {
this.val = val;
}
setVal(val) {
this.val = val;
}
}
let obj1 = {
a: 1,
c: {
d: 4
},
person: new A('obj1'),
arr: [1, [2, 3, 4]]
};
let obj2 = deepClone(obj1);
obj1.arr[0] = 10;
obj1.arr[1][0] = 20;
obj2.person.val = 'obj2'
obj1.person.setVal(666);
obj1.person.addtionFunc = function () {}
console.log(obj1)
console.log(obj2)
/**
obj1
{
a: 1,
c: { d: 4 },
person: A { val: 666, addtionFunc: [Function (anonymous)] },
arr: [ 10, [ 20, 3, 4 ] ]
}
obj2
{
a: 1,
c: { d: 4 },
person: A { val: 'obj2' },
arr: [ 1, [ 2, 3, 4 ] ]
}
*/
复制代码
至此一个能实现基本需求的深拷贝函数已经完成了。还有一种简便的方法,通过JSON序列化来实现深拷贝。本质上也是原始值的复制,因为经过序列化后,对象称为JSON字符串,为原始值,所以相当于原始值的复制。但是有个缺陷,经过JSON序列化的对象,会失去其所有的方法和原型链:
class Person {
constructor(name = '') {
this.name = name;
}
}
let obj1 = {
a: 1, b: 2, func() {
}, person: new Person('zs')
};
console.log(obj1);
//{ a: 1, b: 2, func: [Function: func], person: Person { name: 'zs' } }
console.log(typeof JSON.stringify(obj1));
//string
console.log(obj1.person instanceof Person);
//true
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
//{ a: 1, b: 2, person: { name: 'zs' } }
console.log(obj2.person instanceof Person);
//false
复制代码