相比于其他语言,JavaScript 中的变量可谓独树一帜。正如 ECMA-262 所规定的,JavaScript 变量是 松散类型的,而且变量不过就是特定时间点一个特定值的名称而已。由于没有规则定义变量必须包含什 么数据类型,变量的值和数据类型在脚本生命期内可以改变。这样的变量很有意思,很强大,当然也有不少问题。
原始值与引用值
ES规范 变量可以包含两种不同类型的数据,原始值与引用值;原始值(primitive value)就是 最简单的数据,引用值(reference value)则是由多个值构成的对象。
在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值。 js原始值有6种:String,Number,Boolean,Null,Undefined,Symbol。保存原始值的变量是按值 (by value)访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象,JS不允许直接访问内存位置,所以我们不能直接操作对象所在的内存空间,在操作对象时,我们操作的实际是对象的引用(reference),而不是实际的对象,所以保存引用值的变量是按引用(by reference)访问的
动态属性
原始值和引用值的定义是一样的,都是创建一个变量,然后给变量赋值;当赋值后,能对变量做什么操作就有很大的区别
原始值不能有属性,尽管尝试给原始值添加属性不会报错。 但是打印出来是undefined; 只有引用值可以动态添加后面可以使用的属性,原始值的定义只能使用原始字面量的形式;如果使用 new关键字 则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值
引用值可以随时添加、修改、删除其属性和方法。
let obj = new Object()
obj.age = 18
obj.name = '张三'
console.log(obj.age,obj.name) // 18 '张三'
上面代码首先创建了一个对象,把这对象保存到了变量obj里,再给这个对象添加了age和name属性,分别赋值为18和'张三',我们就可以访问这个新属性,直到对象被销毁或属性被删除
复制值
除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值 到另一个变量时,原始值会被复制到新变量的位置
let str = '张三'
let str1 = str
console.log(str)//'张三'
console.log(str1)//'张三'
str1 = 18
console.log(str)//'张三'
console.log(str1)//18
这里将变量str赋值给变量str1,改变str1的值不会影响到str, 这两个变量可以独立使用,互不干扰
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"
这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
当我们在进行对象复制操作时,需要注意复制的对象的改变是会改变源对象的数据的,如果想避免这种情况,可以使用深克隆,下面提供几种常用的深克隆方法
// 1、利用JSON格式转换进行深克隆
//这种方法存在无法实现对函数、RegExp等特殊对象的克隆(会在JSON.stringify阶段被直接忽略)
//会抛弃对象的constructor,所有构造函数会指向Object
//对象有循环引用,会报错
JSON.parse(JSON.stringify(Object))
//2、使用lodash的cloneDeep 推荐使用
//自己实现一个深克隆函数
/* 使用 WeakMap() 不使用Map() 的原因
WeakMap 是弱引用的键值对集合,键值必须是对象,值可以是任意类型。和 Map 的区别就在于 Map 是强引用,WeakMap 是弱引用。
与Map对象不同的是,WeakMap的键是不可枚举的。不提供列出其键的方法
什么是弱引用呢?弱引用意味着在没有其他引用的情况下,弱引用的对象可以被垃圾回收机制回收。如果一个对象只有弱引用存在,则该对象会在下一次垃圾回收机制执行时被回收。
当我们在深克隆一个很大的对象时,使用 Map 将造成很大的性能损耗,我们必须手动清楚 Map 的键值来释放内存。WeakMap 则不会有这个问题。
*/
function cloneDeep(target, map = new WeakMap()) {
if (typeof target !== 'object') {
return target;
}
if (map.has(target)) {
return map.get(target);
}
// 通过Object.prototype.toString方法,我们可以观察到各种不同对象实例的 toString 都会遵循相同的格式输出,可以比较准确的获取对象的数据类型
const type = Object.prototype.toString.call(target).slice(8, -1);
let cloneTarget;
switch (type) {
case 'Object':
case 'Array':
cloneTarget = type === 'Array' ? [] : {};
map.set(target, cloneTarget);
Object.getOwnPropertyNames(target).forEach(key => {
cloneTarget[key] = cloneDeep(target[key], map);
});
break;
case 'Map':
cloneTarget = new Map();
map.set(target, cloneTarget);
target.forEach((value, key) => {
cloneTarget.set(cloneDeep(key, map), cloneDeep(value, map));
});
break;
case 'Set':
cloneTarget = new Set();
map.set(target, cloneTarget);
target.forEach((value) => {
// 添加某个值,返回Set对象本身
cloneTarget.add(cloneDeep(value, map));
});
break;
case 'RegExp':
case 'Number':
case 'String':
case 'Boolean':
case 'Date':
case 'Error':
// 创建一个新的实例对象
cloneTarget = new target.constructor(target);
break;
case 'Symbol':
cloneTarget = Object(Object.prototype.valueOf.call(target));
break;
case 'Null':
cloneTarget = null;
break;
}
return cloneTarget;
}
传递参数
ECMAScript 中所有函数的参数都是按值传递的,我们可以通过下面两个例子解释一下这句话
// 原始值时
function addNum(num){
return num+=1
}
let num = 1
let fun = addNum(num)
console.log(num)//1
console.log(fun)//2
在函数内部,参数num+1,但是并不会影响外部的原始变量num,外部的num和函数内部的num互不影响。
在看下一个例子
function setName(obj) {
obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
在函数内部,obj 和 person 都指向同一个对象。结果就是,即使对象是按值传进函数的,obj 也会通过引用访问对象。当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。当我们看到这里的时候,会产生怀疑:函数内部的obj添加了一个name属性,外部的person也添加了一个name属性,局部作用域中修改对象而变化反映到全局时,不就意味着参数是按引用传递的吗? 别着急,接着看
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
这里的代码和上面的只加了两行代码,将obj重新定义为一个有name的新对象,person被传入函数时,name被设置为Nicholas,然后obj被设置为一个新的对象并把name属性设置为Greg,如果person是按引用传递, 那么 person 应该自动将 指针改为指向 name 为Greg的对象,name的值应该是 Grag;但是当我们打印的时候发现 name的值还是为Nicholas。这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了
ECMAScript 中函数的参数就是局部变量
确定类型
typeof 操作符最适合用来判断一个变量是否为原始类型,更确切一点是用来判断一个变量是否是String,Number,Boolean和Undefined,如果变量为null会返回为“object”
typeof一般用于对原始值的类型判断,因为我们一般不关心值是不是一个对象,而是关心是个什么类型的对象,所以就有了instanceof操作符 。instanceof只能用来判断对象、函数和数组
还有一个不错的判断类型的方法,就是Object.prototype.toString.call().slice(8, -1),我们可以利用这个方法来对一个变量的类型来进行比较准确的判断
执行上下文和作用域
上下文分为三种
全局作用域
在浏览器中全局上下文相当于window对象,所有通过var声明的全局变量和函数都会成为window的属性和方法
上图所示,我们定义了一个全局变量color和一个函数fun,我们可以在window对象上找到我们定义的color和fun();全局作用域在页面创建时就创建了,在页面销毁时销毁。
函数作用域
在函数作用域里可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量
当在函数作用域中操作一个变量时,会先在自身作用域中寻找,如果有直接使用,如果没有会去上级作用域寻找,一直找到全局作用域,如果全局作用域还是没有就会报错。这就是作用域链。
如果在函数中直接 a = 30 ,那么全局的变量a会被赋值 30,因为我们使用 a = xx 这种方法声明变量的时候===window.a = xx,会改变全局中a的值
那在函数中想访问全局变量怎么办呢?我们可以使用window对象来访问。
注意:函数的形参就相当于在函数作用域中声明的了变量
声明提升
- 变量声明提升
使用var关键字声明的变量会被提升。但是变量的赋值不会被提升
console.log(a)
var a = 10
上面代码执行的结果是 undefined。他被解析成了下面的结果
var a
console.log(a)
a = 10
现在我们选择不用 var,改用更合理的 let 和 const
- 函数声明提升
使用函数声明形式创建的函数会在代码执行前被创建,所以我们可以在函数声明前调用
使用函数表达式创建的函数声明会被提升,但是赋值不会被提升,所以我们使用函数表达式创建函数时,不能在声明前使用
垃圾回收
自己还没搞清楚,搞清楚在写。。。。