2. 变量,作用域与内存

前言

JavaScript 变量是松散类型的,而变量不过就是特定时间点的一个特定值的名称而已。
由于没有规则定义变量必须包含什么数据类型,变量的值和数据类型在脚本生命期内可以改变。
这样的变量很有意思,很强大,当然也有不少问题。

原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:
  - 原始值(primitive value):就是最简单的数值
  - 引用值(reference value):由多个值构成的对象

引用值是保存在内存中的对象。
在操作对象时,实际上操作的是对该对象的引用(reference)而非实际的对象本身,保存引用值的变量是 按引用(reference)访问的。

在很多语言中,字符串是使用对象表示的,因此被认为是引用类型,ECMAScript 打破了这个惯例

动态属性

原始值

原始值不能有属性,尽管尝试给原始值添加属性不会报错

原始类型的初始化可以只使用字面量形式;

原始类型的初始化如果使用的是 new 关键字,则 JS 会创建一个 Object 类的实例,但其行为类似原始值。

let name1 = "Nicholas";
let name2 = new String("Matt");

name1.age = 17;
name2.age = 18;

console.log('name1.age:  ', name1.age);
console.log('name2.age:  ', name2.age);

console.log('typeof name1', typeof name1)
console.log('typeof name2', typeof name2)

引用值

引用值:可以随时添加,修改和删除其属性和方法

let person = new Object();
person.name = "Nicholas";
console.log(person);

复制值

原始值

通过变量把一个原始值赋值到另一个变时,原始值会赋值到新变量的位置

let num1 = 5;
let num2 = num1;
// num1 和 num2 中的 5 是完全独立的(这两个变量可以独立使用,互不干扰)

引用值

把引用值从一个变量赋给另一个变量时,存储在变量中的值也会赋值到新变量所在的位置(复制的其实是一个指针,指向存储在堆内存中的对象)

两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来

let obj1 = new Object();
let obj2 = obj1;

obj1.name = "Nicholas"

console.log('obj1.name:  ', obj1.name)
console.log('obj2.name:  ', obj2.name)

传递参数

ECMAScript 中所有函数的参数都是按值传递的
引用值:与引用值变量的复制一样

ECMAScript中,函数的参数就是局部变量

函数(原始值参数传递)

原始值:与原始值变量的复制一样

function addTen(num) {
    num += 10
    return num
}

let count = 10;
let result = addTen(count);

console.log('count: ', count);
console.log('result: ', result)

函数(引用值参数传递)

引用值:与引用值变量的复制一样

function setName(obj) {
    obj.name = "Nicholas1";
    obj = new Object();  
    // 当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。
    // 而那个本地对象在函数执行结束时就被销毁了
    obj.name = "Nicholas2"
}

let person = new Object()
setName(person)
console.log(person)

即使对象是按值传进函数的,obj 也会通过引用访问对象

确定类型

typeof

typeof 适合用来判断一个变量是否为原始类型(字符串,数值,布尔值 或 Undefined)
如果值是 对象 或 null,那么 typeof 返回 “object”

let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();

console.log('typeof s:  ', typeof s);  // string
console.log('typeof b:  ', typeof b);  // boolean
console.log('typeof i:  ', typeof i);  // number
console.log('typeof u:  ', typeof u);  // undefined
console.log('typeof n:  ', typeof n);  // object
console.log('typeof o:  ', typeof o);  // object

instanceof

语法如下:

result = variable instanceof constructor

如果变量是给定引用类型,则 instanceof 操作符返回 true

let person = new Object();

console.log('person instanceof Object: ', person instanceof Object);
console.log('person instanceof Array: ', person instanceof Array);
console.log('person instanceof RegExp: ', person instanceof RegExp);
按照定义:
  - 所有引用值都是 Object 的实例
  - 通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true
  - 通过 instanceof 操作符检测原始值,始终返回 false

执行上下文与作用域

作用域链

上下文中的代码在执行的时候,会创建变量对象的一个 作用域链(scope chain)
这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序
代码正在执行的上下文的变量对象始终位于作用域链的最前端。

全局上下文

在浏览器中,全局上下文就是我们常说的 "window" 对象
  - 通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法
  - 使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链上效果是一样的

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数
(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

函数上下文

var color = "blue";

function changeColor(){
    let anotherColor = "red";

    function swapColors(){
        let tempColor = anotherColor;
        anotherColor = color;
        // 这里可以访问 color, anotherColor 和 tempColor
    }
    // 这里可以访问 color, anotherColor 但访问不到 tempColor
    swapColors()
}

// 这里只能访问 changeColor
changeColor()

/*
- 作用域链的最前端: window
    - color
    - changeColor
        - anotherColor
        - swapColors
            - tempColor
*/

函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则

作用域链增强

某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除
  - try/catch 语句的 catch 块
  - with 语句

对于 with 语句来说,会向作用前端添加指定的对象

function buildUrl() {
    let qs = "7debug=true"

    with (location) {
        let url = href + qs
    }
    return url  // with 内部使用 let 声明会导致报错(使用 var 会正常执行)
}

// 在 with 语句用 var 声明的变量 url 会成为函数上下文中的一部分,可以作为函数的返回值返回
// 在 with 语句用 let 声明的变量 url 会被限制在块级作用域,所以在 with 块之外没有定义
let result = buildUrl()
console.log(result)

变量声明

使用 var 在函数作用域声明

使用 var 声明变量时,变量会自动添加到最接近的上下文(函数中,最接近的上下文就是函数的局部上下文)

function add(num1, num2){
    var sum = num1 + num2;
    return sum
}

let result = add(10, 20);
console.log(sum);  // 报错,sum 在当前上下文中不是有效变量

如果变量未经声明就被初始化了,它就会被自动添加到全局上下文

function add(num1, num2){
    sum = num1 + num2;
    return sum
}

let result = add(10, 20);
console.log(sum);

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

var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。(这个现象叫做 ”提升“)
”提升“ 让同一作用域内的代码不必考虑变量是否已经声明就可以直接使用

在声明之前打印变量,可以验证变量会被提升(声明的提升意味着会输出 undefined 而不是 Reference Error)

console.log(name);  // undefined
var name = "Jake";
var name = "Jake";
// 等价于
name = "Jake"
var name
console.log(name)

//
function fn1(){
    var name = "Jake";
}
// 等价于 
function fn2(){
    var name;
    name = "Jake";
}

使用 let 的块级作用域声明

let 关键字的作用域是块级的(块级作用域由最近的一对花括号 {} 界定)
if块,while块,function块,单独的块都是 let 声明变量的作用域

if (true){
    let a;
}
console.log(a);  // Reference Error: a is not found
while(true){
    let b;
}
console.log(b);  // Reference Error: b is not found
function foo(){
    let c;
}
console.log(c);  // Reference Error: c is not found
// 独立的块
{
    let d;
}
console.log(d);  // Reference Error: d is not found

let 与 var 的不同之处是在同一作用域内不能声明同一个变量两次,会抛出 SyntaxError

重复的 var 声明会被忽略,重复的 let 会抛出

var a;
var b;

{
    let b;
    let b;  // SyntaxError
}

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 is not defined

let 也会被提升,但由于 ”暂时性死区“的缘故,实际上不能再声明之前使用 let 变量(let 的提升跟 var 是不一样的)

console.log(name);  // ReferenceError: name is not defined
let name;

使用 const 常量声明

使用 const 声明的变量必须同时初始化为某个值

一经声明,在其声明周期的任何时候都不能赋予新值

const a;  // SyntaxError 常量声明时没有初始化
const b = 3;
console.log(b);
b = 4;  // TypeError: 给常量赋值

const 除了要遵循上述的规则,其他方面与 let 是一样的

赋值为对象的 const 变量不能再被赋值为其他引用值,但对象的键不受限制

const o1 = {};
o1 = {}; // TypeError: Assignment to constant variable.(给常量赋值)
const o2 = {};
o2.name = "Jake";
console.log(o2.name);

如果想让整个对象都不能修改,可以使用 Object.freeze() 再给属性赋值时虽然不会报错,但会静默失败

const o3 = Object.freeze({});
o3.name = "Jake";
console.log(o3)
console.log(o3.name)

标识符查找

假设当前作用域为局部作用域:
  - 先从当前作用域(局部作用域)搜索标识符,搜索到了停止搜索
				↓(假设局部作用域只有一层)
  - 再往上查找(全局作用域)搜索标识符,搜索到了停止搜索
var color = "blue";
function getColor(){
    let color = "red";
    return color;
}
console.log(getColor());  // 会在函数作用域(局部作用域内)先搜索 color 变量,搜索到了就停止
var color = "blue";
function getColor(){
    let color = "red";
    {
        let color = "green"
        return color;
    }
}
console.log(getColor());
// 因为 return 在块级作用域中(会先从块级作用域中搜索,搜索到了就返回)
// 否则会继续向上搜搜(函数作用域) 再向上搜索(全局作用域)

标识符的查找并非没有代价。访问局部变量比访问全局变量要快。因为不用切换作用域

  • 46
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值