JavaScript 变量和作用域

JavaScript 变量和作用域

学习一门语言,首先要学习的就是,这门语言如何表达信息和处理信息.在上一篇中,讲解了JavaScript的基本语法,了解JavaScript 的基本如何表达信息.本篇将更加深入的讲解,JavaScript的信息的表达. 将遵循如下图的讲解.


在上一篇文章中,我们都知道了,JavaScript中的基本类型有:undefined、null、Number、Boolean、String.这5种基本类型. 任何语言变量的本质都是:操作数据和保存数据的容器. 下面我们看一下JavaScript另一个重要的类型:引用类型

引用类型

直接看下面代码,引用类型的例子:
我们通过{}来创建了一个引用类型,引用类型支持添加属性和方法.

var preson = {};
//添加属性
preson.text ='小明';
preson.sex ='男';
//添加方法
preson.action = function(){
console.log('action');
}
console.log(preson.name);//调用属性
delete preson.sex;//删除属性
preson.action();//调用方法

常用的是在{}里面添加属性和方法,调用属性和方法与上述代码一致.

var preson = {
text:'小明',
sex:'男',
action:function(){
console.log('action');
}
};
console.log(preson.text);//调用属性
delete preson.sex;//删除属性 只是删除属性 注意:不可以删除方法 删除不了变量 同时也不能删除原型链中的属性
preson.action();//调用方法

[]表示的变量也是引用类型,代码如下.[]表示的是一个数组

var person = [1,-1,2,3];//字面量表示法
console.log(person[0]);//1
console.log(person[1]);//s
var names = Array['te','m3'];

需要注意的是,函数也是引用类型,可以给函数添加属性和方法.

function fn(){

}
fn.name = 'xm';
console.log(fn.name);//xm

同时还有Object肯定是引用类型,为什么呢,因为所有的引用类型都是Object的孩子,我会在下面讲解.

var obj = new Object();
obj.name = '1';
obj.action = function(){
console.log('test');
}
obj.action();

obj.action instanceof Object //true 返回true 可见function 也是Object类型

在上述例子中,Array Map 函数 Object 都是引用类型,那么他们的typeof打印出来的是什么? 从下面代码中,可以得出其实引用类型就是Object类型.

typeof []
"object"
typeof {}
"object"
typeof Object()
"object"
typeof function add(){}
"function" //而function instanceof Object 返回true 那么function也是Object类型

引用类型和基本类型有什么区别?

基本类型保存在栈内存中;引用类型保存在堆内存中,那么其实引用类型的变量就是保存指向堆内存的地址.

如上图所示,在栈内存中,我们保存着基本类型的值,为什么基本类型的值要保存在栈内存中呢? 栈内存中大小是固定的,不支持扩展内存,而基本类型也不会去扩展内存.只有引用类型,大小是不固定的,我们可以随意的添加属性和方法,所以引用类型的值,只能保存在堆内存中,在堆内存中开辟一块空间,可以随时扩展,而栈内存中就保存了指向堆内存的地址,当我们获取引用类型的数据时,就会从栈内存中取出指向堆内存的地址,然后去堆内存中查找该地址,取出保存的值. 引用类型过多的使用会导致内存的增加,所以要合理的使用引用类型.

如何判断引用类型是否相等

我们知道,判断基本类型相等,只需要使用=====, ==比较的是值,===比较的是值和类型.
我们在看一下引用类型是否可以这样判断呢?如下述代码,xm、xh虽然属性值相等但是 两个的 === 并不相等,因为xm xh 在堆内存上开辟了两个不同的空间,它们两个存储的内存地址也是不一样的.

var xm = {
name:'小明',
sex:'女'
}

var xh = {
name:'小明',
sex:'女'
}
//不相等 引用类型 只有指向同一个引用才相等 xm xh 开辟了两个不同的空间
console.log(xm === xh);//false
//可以使用遍历对象 判断属性是否相等判断
function equleObj(a,b){
for(var x in a){
if(a[x] !== b[x]) return false;
}
return true;
}
//也可以使用 两个指向同一个堆内存空间
var xh = xm;
xm === xh;//true

变量的复制

  1. 基本类型变量的复制
    看下面代码,直接从代码层次上理解:
    大致意思是:a = 1,创建了变量b 的值等于a,然后改变变量b的值为2,实际输出结果b=2,a=1 变量a的值并没有改变,也就是说基本类型的变量复制,只是将变量的值复制给了另一个变量,而不影响变量的值.
a = 1;
b = a;
b = 2;
console.log('b:'+b+' a:'+a);//b = 2 ; a = 1;
  1. 引用类型变量的复制
    看下面代码:
    我们将变量a设置成一个对象,添加了name属性=‘xm’,然后将变量a复制给变量b,然后变量b.name = ‘xh’,这时变量a的name属性的值也变成了’xh’,这个结果,可能跟你想象中的不一样,为什么引用类型的复制会改变被复制的引用类型的属性值呢?
    在上述讲解中,我们知道引用类型,在栈内存中存储的是指向堆内存的地址,那么变量的复制其实就是将栈内存中存储的值赋给另一个变量,比如:a = 0x23928329(堆内存的地址), b = a, b = 0x23928329(堆内存的地址),a和b都指向了一个堆内存中,不管是a改变了还是b改变了,它们都是改变同一个堆内存中的值.
a = {
name:'xm'
};
b = a;
b.name = 'xh';
console.log('b:'+b.name+' a:'+a.name);//b:xh a:xh

JavaScript 的解析机制和作用域

JavaScript 的解析机制

在了解作用域之前,我们先来了解,JavaScript是如何进行解析代码的.
JavaScript中,执行代码之前进行了:预解析 —> 解析 —> 执行.来分析一下.预解析的过程.

  • 预解析
    直接上代码,从代码的角度讲解,更容易理解.
    下面代码大致意思:输出name的值,但是name输出的是undefined?
var name = 'xm';
var age = 18;
function fn(argument) {
console.log(name);//undefined
var name = 'xh';
var age = 20;
}
fn();

预解析过程如下:
预解析是通过层级来进行解析的,
首先通过window层,进行预解析,name = undefined -> age = undefined -> fn().
然后预解析fn函数:
name = undefined -> age = undefined -> arguments = undefined
然后进行逐行解析:
window: name = ‘xm’ age = 18 fn()
然后执行fn,这个时候fn中的局部变量
name = undefined -> age = undefined -> arguments = undefined
console.log(name);//name = undefined
而name是在console.log() 执行后才赋值的.

预解析
window
name = undefined
age = undefined
fn(){....}
fn
name = undefined
age = undefined
argument = undefined
//然后逐行解析代码
window
name = 'xm'
age = 18
fn(){....} //跳过
执行fn 这时候局部变量name并没有被解析执行 所以得到的就是undefined
fn
name = undefined
age = undefined
argument = undefined
console.log(name);//name = undefined;
name = 'xh'
age = 20

OK,我们了解了JavaScript的预解析 —> 解析 —> 执行的过程,那么JavaScript的作用域就很容易理解了.

关于预解析需要注意以下几点:
不要在if 或者 for中定义函数

  1. 以var 声明的变量 会预解析
  2. 以let 声明的变量 不会进行预解析
  3. 函数的声明会预解析
  4. 函数表达式不进行预解析

注意函数在预解析时,已经提前进行了声明. 如下代码: 函数在预解析之前就已经声明了,所以你可以在函数之前调用函数,而不会报错.

document.write(fn1);//输出 function fn1(params){}
function fn1(params) {}

再看如下解析,将函数赋值给一个变量,注意以var声明的变量,会进行预解析,那么在变量的前面去调用变量,会得到undefined的错误.

document.write(fn2);//undefined
//注意这是变量赋值不是函数声明 预解析后的fn2 = undefined
var fn2 = function(){};

JavaScript 解释器中存在一种变量声明被提升(hoisting)的机制,也就是说变量(函数)的声明会被提升到当前作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。

作用域

我们直接用代码来理解,作用域的特点

 console.log(a);// 1 
var a = 1;

console.log(a);//报错 not defined
a = 1;//没有var 就不能正常的预解析

console.log(a);//function a(){console.log(4);}
var a = 1;
console.log(a);//1
function a() {
console.log(2);
}
console.log(a);//1
var a = 3;
console.log(a);//3
function a() {
console.log(4);
}
console.log(a);//3
a();//报错

上述代码的解析过程就是:

/**
* 执行过程,Js在预解析时 提前进行了函数的声明,对于同名的函数
* 最后一个会覆盖前一个
* 所以第一个log,输出最后一个函数声明
* 预解析的结果 函数名和变量名冲突则变量名被移除
* a = undefined
* a(){..} a= undefined 移除
* a = undefined
* a(){..} a = undefined 移除
*
* 解析过程
* 输出log
* 声明了变量a 函数a()移除
* 1
* 1
* 3
* 3
*
*
*/

关于作用域的问题,挺难理解的,我会在单独出一篇专门讲作用域的文章,大家也可以去看《你不知道的JavaScript上卷》第一章就是讲解作用域的.

文章作者: JakePrim
文章链接: https://jakeprim.cn/2019/08/01/javascript-2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 JakePrim技术研究院
打赏