变量与作用域

一、原始值与引用值

变量 : 原始值(最简单的数据)、引用值(多个值构成的对象)

原始数据类型: Undefined、Null、Boolean、Number、String 和 Symbol。
引用数据类型:Object、Array、function

动态属性

对于引用值,可以随时对其进行 添加、修改和删除其属性和方法 。

let user = new Object();
//创建对象保存在user中。添加一个name的属性和方法,赋值一个字符串,这时候就可以访问到name属性,直到对象被销毁或属性被显式的删除
user.name = "张三"
console.log(user.name) //张三

而原始值不能有属性:
let name = "张三"
name.age = 27
console.log(name.age) //undefined  只有引用值才能动态的添加后面使用的属性

❗注意:
原始类型的初始化可以只使用原始字面量的形式。如果使用的是new关键字,则Javascript会创建一个Object类型的实例,但是他的行为类似原始值。

let name1="张三"
let name2=new String("李四")
name1.age=12
name.age=23
console.log(name1.age) //undefined
console.log(name2.age) //23
console.log(typeof name1) //String
console.log(typeof name2) //Object

复制值

原始值和引用值在通过变量复制时也有所不同 。通过变量将一个原始值赋值给另一个变量的时候,原始值就会被复制到新的变量的位置。

let num1=5
let num2=num1

这里我们将num1赋值给了num2,那么这时候num2的值和存储在num1中的值是完全独立的,他们互不干扰,如下图所示。
在这里插入图片描述

将引用值从一个变量赋值给另外一个变量时,存储在变量中的值也会被复制到新的变量所在的位置。这里复制的值实际是一个指针,他指向存储在堆内存中的对象。操作完成后,两个变量实际指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "张三";
console.log(obj2.name) //张三

变量 obj1 保存了一个新对象的实例。 这个值被复制到 obj2,此时两个变 量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为 它们都指向同一个对象。

传递参数

函数外的值会被复制到函数内部的参数中,像从一个变量复制到另一个变量一样。如果只原始值,那么就跟原始值变量复制一样,如果是引用值那么就和引用值一样。在按值传递参数的时候,值会被复制到一个局部变量(即一个命名参数可以理解为形参, 或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位 )。在按引用值传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部( 这在 ECMAScript 中是不可能的。)

function addTen(num){
  num +=10;
  return num;
}
let count =20;
let result = addTen(count);
console.log(count); //20
console.log(result); //30

在示例函数当中,num是一个局部参数,是按原值传入的,传入的实参是count这个变量,其值为20,在这里打印出的result这个值为30,但count并未被修改为30。如果是按引用传入,那么count的值也会被修改为30,接下来看下面的例子。

function setName(obj){
  obj.name = "张三"
}
let person = new Object();
setName(person); //调用函数,传入参数person
console.log(person.name) //张三

在此函数当中,我们将person这个对象作为参数传入,并且被复制到参数obj当中。在函数内部,obj与person都指向同一个对象。即使对象是按值传进函数的,obj也会通过引用访问对象。当函数内部给obj设置了name属性时,函数外部的对象也会反映这个变化, 因为 obj 指向的对象保存在全局作用域的堆内存上。 很多开发者错误地认为, 当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。为证明对象是按值传 递的,我们再来看看下面这个修改后的例子:

function setName(obj){
  obj.name = "张三";
  obj = new Object();
  obj.name = '李四'
}
let person = new Object();
setName(person);
console.log(person.name);

总结:在第二个例子当中,在 setName()中多了两行代码,将 obj 重新定义为一个有着不同 name 的新对象。
当person作为一个参数传入函数时,其name属性被设置为“张三”。之后obj被设置为一个新的对象,并且将name属性被设置为“李四”。如果
person 是按引用传递的,那么 person 应该自动将 指针改为指向 name 为"Greg"的对象。可是,当我们再次访问
person.name 时,它的值是"Nicholas", 这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj
在函数内部被重写时,它变成了一个指 向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。

注意 ECMAScript 中函数的参数就是局部变量。

确定类型

typeOf操作符最适合用来判断一个变量是否为原始类型。如果值是null,那么typeOf返回“Object”。

let str = "张三";
let bl = true;
let num = 22;
let u;
let n = null;
let obj = new Object();
console.log(typeof str); // string
console.log(typeof num); // number
console.log(typeof bl); // boolean
console.log(typeof u); // undefined
console.log(typeof num); // object
console.log(typeof obj); // object 

当我们想知道一个值是什么类型的对象的时候,这样typeOf操作符就不适用了,因此ECMAScript 提供了 instanceof 操作符。
语法:

result = variable instanceof constructor
 如果变量是给定引用类型的实例,那么instanceof操作符 操作 符返回 true。
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

instanceof 操作符检测任何引用值和Object 构造函数都会返回true。类似地,如果用 instanceof
检测原始值,则始终会返回 false, 因为原始值不是对象。

二、执行上下文作用域

作用域链增强

执行上下文需要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象:

a. try/catch 语句的 catch 块
b. with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添 加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误 对象的声明。看下面的例子:

function buildUrl(){
  let qs = '?debug=true'
  with(location){
    let url = href +qs;
  }
  return url;
}

with语句将location对象作为上下文,因此location会被添加到作用域链前端。buildUrl()函数中定义了一个变量qs。当with语句中的代码引用变量href时,实际上引用的是location.href ,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,他定义在函数上下文的变量对象上。 而在 with 语句中使用 var 声明的变量 url 会成为函数 上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作 用域,所以在 with 块之外没有定义。

变量声明

使用 var 的函数作用域声明

使用var声明变量时,遵循就近原则。在函数中,那么最近的就是函数的局部上下文。在with语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么他就会自动被添加到全局上下文。

function add (){
  var sum = num1+num2;
  return sum
}
let result =add (10,20); //30
console.log(sum) //报错:num1 is not defined(sum 在这里未定义,不是有效变量)

这里,函数 add()定义了一个局部变量 sum,保存加法操作的结果。这个值作为函数的值被返回, 但变量 sum 在函数外部是访问不到的。如果省略上面例子中的关键字 var,那么 sum 在 add()被调用 之后就变成可以访问的了,如下所示:

function add(num1, num2) {
 sum = num1 + num2;
 return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 30

这一次,变量 sum 被用加法操作的结果初始化时并没有使用 var 声明。在调用 add()之后,sum 被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。

注:在初始化变量之前一定要先声明变量。 在严格模式下,未经声明就初始化变量 会报错。

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升” (hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提 升也会导致合法却奇怪的现象,即在变量声明之前使用变量。
示例1:

var name = "Jake";
name = 'Jake';
var name; 

示例2:

function fn1() {
 var name = 'Jake';
} 
function fn2() {
 var name;
 name = 'Jake';
} 

以上示例中的两个代码块是等价的, 通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是 Reference Error((引用错误)对象代表当一个不存在(或尚未初始化)的变量被引用时发生的错误。):

console.log(userName); // undefined
var userName = 'Jake';
function fn() {
 console.log(userName); // undefined
 var userName = 'Jake';
}
fn()

使用 let 的块级作用域声明

ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独 的块也是 let 声明变量的作用域。

if (true) {
 let a;
}
console.log(a); // ReferenceError: a 没有定义
while (true) {
 let b;
} 
console.log(b); // ReferenceError: b 没有定义
function foo() {
 let c;
}
console.log(c); // ReferenceError: c 没有定义
 // 这没什么可奇怪的
 // var 声明也会导致报错
// 这不是对象字面量,而是一个独立的块
// JavaScript 解释器会根据其中内容识别出它来
{
 let d;
}
console.log(d); // ReferenceError: d 没有定义

let 与 var 的不同之处:
var 可以在同一作用域声明两次, 重复的 var 声明会被忽略。
let不可以在同一作用域声明两次,重复的 let 声明会抛出 SyntaxError(已经被声明过了)。

var a;
var a;//不会报错
{
  var b;
  var b;  // SyntaxError: 标识符 b 已经声明过了
}

let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情 况应该避免。来看下面两个例子:

for (var i = 0; i < 10; ++i) {}
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义

严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的 缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var 是不一样的。
使用 const 的常量声明
使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const a; // SyntaxError: 常量声明时没有初始化

const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值
 const 除了要遵循以上规则,其他方面与 let 声明是一样的:  
if (true) {
 const a = 0;
}
console.log(a); // ReferenceError: a 没有定义
while (true) {
 const b = 1;
}
console.log(b); // ReferenceError: b 没有定义
function foo() {
 const c = 2;
}
console.log(c); // ReferenceError: c 没有定义
{
 const d = 3;
}
console.log(d); // ReferenceError: d 没有定义

const 声明只应用到顶级原语或者对象。 简单来讲就是赋值为对象的cost变量不能再被重新赋值为其他引用值,但对象的键不受影响。

const o1 = {};
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
 如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错, 但会静默失败:  
const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined 

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例 都替换成实际的值,而不会通过查询表进行变量查找。
注意 开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用 const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现 重新赋值导致的 bug。
标识符查找
当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。 如果找到该标识符那么停止搜索,变量确定,如果没有找到,那么继续沿作用域链搜索。 。(注意,作用域链中的对象也有一个 原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。 如果仍然没有找到标识符,则说明其未声明。

var color = 'blue';
function getColor(){
  return color;
}
getColor()
console.log(getColor()); // 'blue'

在这个例子当中调用函数时会引用变量color。 为确定 color 的值会进行两步搜索。 第一步,搜索 getColor()的变量对象,查找名为 color 的标识符。结果没找到,于是继续搜索下一 个变量对象(来自全局上下文),然后就找到了名为 color 的标识符。因为全局变量对象上有 color 的定义,所以搜索结束。
如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符

var color = 'blue';
function getColor() {
 let color = 'red';
 return color;
}
console.log(getColor()); // 'red'

使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加额外的层次:

var color = 'blue';
function getColor() {
 let color = 'red';
 {
 let color = 'green';
 return color;
 }
}
console.log(getColor()); // 'green'

。在调用这个函数时, 变量会被声明。在执行到函数返回语句时,代码引用了变量 color。s于是开始在局部上下文中搜索这个 标识符,结果找到了值为’green’的变量 color。因为变量已找到,搜索随即停止,所以就使用这个局 部变量。这意味着函数会返回’green’。在局部变量 color 声明之后的任何代码都无法访问全局变量 color,除非使用完全限定的写法 window.color。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

辛-夷

你的鼓励就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值