变量类型
原始值与引用值
原始值:就是最简单的数据,包含Undefined、Null、Boolean、Number、String、Symbol。我们对其的操作和访问都是对值进行的。
引用值:保存在内存中的对象,因为js无法操作内存空间,所以我们对其操作和访问都是针对引用。
主要区分将会体现在动态属性、复制值、传递参数、确定类型四个方面。
动态属性
动态属性简单来说就是动态的赋值一个属性,对象可以直接通过obj.的形式赋值,但是当给一个原始值类型的变量赋值时会发生什么呢。比如
let name = 'ganlanshu';
name.description = 'yanhuo';
console.log(name);
// 这样去操作一个字符串并不会报错,但是当你打印的时候则会报undefined。
// 删除一个对象的属性
delete name.description;
Reflect.deleteProperty(name, 'description');
// 这两个方法也可以用于操作数组,但是当对象的属性的description中configurable
配置为false时,删除则是无效的。
通过Object.defineProperty(targetObj,prop,description)配置
复制值
这个就比较简单了,原始值赋值过去的值改变后并不会影响旧值。而引用变量由于是多个索引指向同一块内存,改变一个,当其他的访问时也会发生变化。
传递参数
原始值传参与引用传参都是按照值传递。引用传参为什么也叫做按照值传递,就不是很容易理解。 当我们传递参数时,值会被复制给一个局部变量。 原始值传参
function add(num){
num + = 10;
return num;
}
let count = 20;
add(count);
console.log(count); // 20
function setName(obj){
obj.name = 'ganlanshu';
obj = new Object();
obj.name = 'yanhuo';
obj.age = 27;
}
let obj = {name: 'feiniaotoulin'};
setName(obj);
console.log(obj)
对于引用值来说,相当于把这个对象的内存地址赋值给了这个命名参数,而对这个命名参数本身的修改是不会影响原来obj的。
确定类型
我们如何确定一个值是原始值还是引用类型,假如某个值是引用类型我们又该如何确定他是Object还是Function呢。 对于原始值的确认,我们可以用typeOf去判断。但是对于null,假如我们
let test = null;
console.log(typeof test); // object
这是因为对于null来说他在内存中的机器码为一堆0,而标志对象类型的机器码为000,所以就会有些冲突。
对于引用值我们可以根据instanceof判断,instanceOf用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
当然不想出错的话,这有一个更好的方法。
Object.prototype.toString.call(https://wos.58cdn.com.cn/IjGfEdCbIlr/ishare/97b507f0-4cd5-4653-8888-054535f07335截屏2022-07-04 下午8.53.15.png) // "[object Number]"
Object.prototype.toString.call('hi') // "[object String]"
Object.prototype.toString.call({a:'hi'}) // "[object Object]"
Object.prototype.toString.call([1,'a']) // "[object Array]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(() => {}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(Symbol(https://wos.58cdn.com.cn/IjGfEdCbIlr/ishare/603501ae-87b9-4707-b357-e189c197b8a7截屏2022-07-05 下午2.34.52.png)) // "[object Symbol]"
执行上下文与作用域
变量或函数的上下文决定他们可以访问哪些变量以及他们的行为。每个上下文都有一个关联的变量对象。 全局上下文是最外层的上下文,就是我们常说的window对象。所有var定义的全局变量和函数都会成为window对象的属性和方法(对象环境记录)。let与const则不会(声明性环境记录)。 详见:segmentfault.com/a/119000001…
变量声明
var声明变量: 声明时被自动添加到最接近的上下文,如果未经声明就初始化则会被自动添加到全局上下文。存在变量提升。
let声明变量: 作用域是块级的,变量不能重复声明,也会被提升但是因为暂时性死区,所以在声明之前不能使用。
暂时性死区: 当程序的控制流程在新的作用域进行实例化时,在此作用域中用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定(对声明语句进行求值运算),所以不能被访问(访问就会抛出错误)。所以在这运行流程进入作用域创建变量,到变量开始被访问之间的一段时间,就称之为TDZ(暂时性死区)。
const声明变量: 声明时必须被初始化为某个值。用const声明的变量的值不能再改变,引用类型内部键则不受影响。如果不想引用类型被修改可以使用Object.freeze(obj)。
建议声明时使用const声明,这样编译器运行时会将实例替换成实际的值而不需要通过查询表进行变量查找。
作用域链
function bar() {
console.log(myName)
}
function foo() {
var myName = "haha"
bar()
}
var myName = "heihei"
foo()
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。 调用栈如下:
从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链,在 JavaScript 执行过程中,其作用域链是由词法作用域决定的,词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
块级作用域中的变量查找
function bar() {
var myName = "bigJs"
let test1 = 100
if (1) {
let myName = "Chrome"
console.log(test)
}
}
function foo() {
bar();
}
var myName = "heihei"
let test = 1
foo()