【高级程序设计(第四版) 】- 变量、作用域与内存

390 阅读9分钟

原始值与引用值

  ECMAScript变量包含两种不同的类型: 原始值引用值原始值就是6种基本数据类型,原始值保存在中,按值存储引用值保存在内存(堆)中,在栈中保存对其的引用而非引用值本身,保存引用值的变量是按引用访问的。

动态属性

  引用值可以随时添加、修改和删除其属性和方法; 但是原始值不能有属性,原始值有方法的根本原因是: 每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。但该行代码过后该原始包装类型就会被销毁.

复制值

  原始值引用值都是从一个变量赋值给另外一个变量,存储在变量中的值也会被复制到新变量所在的位置。原始值引用值的复制存在差异:原始值是值本身,但是变量存储的引用值实际是一个指针,指向存储在堆内存中的对象,引用值复制之后 指针一致,指向同一个对象,会相互影响

  • 原始值
    let num1 = 5;
    let num2 = num1;
    
    num1与num2是完全独立使用的,互不干扰。 原始值复制
  • 引用值
var obj1 = new Object(); 
var obj2 = obj1; 
obj1.name = "Nicholas"; 
console.log(obj2.name); //"Nicholas"

变量 obj1 保存了一个对象的新实例。然后,这个值被复制到了 obj2 中;换句话说,obj1 和 obj2 都指向同一个对象。这样,当为 obj1 添加 name 属性后,可以通过 obj2 来访问这个属性,因为这两个变量引用的都是同一个对象 引用值复制

传递参数

  ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。

// 原始值(基本类型值)
function addTen(num) { 
  num += 10; 
  return num; 
} 
let count = 20;
// 将count就位参数传入,这个值被复制给参数num在addTen()内部使用,变量count和参数num相互独立,互不干扰
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30
// 引用值(引用类型值)
  function setName(obj) { 
   obj.name = "Nicholas"; 
  } 
  let person = new Object(); 
  // 变量person作为参数,并被复制到参数obj中,在函数内部,其实obj与person都指向同一个函数,函数内部修改了参数obj的值,函数外部的person变量也会改变
  setName(person); 
  console.log(person.name); // "Nicholas"

检测类型

  • typeof   typeof操作符用适合用来判断一个变量是否为原始类型,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式。
 "undefined" — 未定义 typeof undefined
 "boolean"   — 布尔值 typeof boolean
 "string"    — 字符串 typeof string
 "number"    — 数值 typeof number
 "object"    — 对象或null  typeof object/null
 "function"  — 函数 typeof function
  • instanceof   instanceof操作符适合用来检测引用类型,可以检测到它是什么类型的实例。instanceof 检测一个对象A是不是另一个对象B的实例的原理是:查看对象B的prototype指向的对象是否在对象A的[[prototype]]链上。如果在,则返回true,如果不在则返回false。不过有一个特殊的情况,当对象B的prototype为null将会报错(类似于空指针异常).
 console.log(person instanceof Object); // 变量 person 是 Object 吗?
 console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
 console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?
  • Constructor constructor属性返回对创建此对象的数组函数的引用。可以用于检测自定义类型
 'xz'.constructor == String // true
 (123).constructor == Number // true
 (true).constructor == Boolean // true
 [1,2].constructor == Array // true
 ({name:'xz'}).constructor == Object // true
 (function(){}).constructor == Function // true
 (new Date()).constructor == Date // true
 (Symbol()).constructor == Symbol // true
 (/xz/).constructor == RegExp // true

constructor不适用于null和undefined。除了这些原生的,constructor还可验证自定义类型

function Xzavier(){}
 var xz = new Xzavier();
 xz.constructor == Xzavier;  // true 
  • Object.prototype.toString.call(obj) 原理:调用从Object继承来的原始的toString()方法
 Object.prototype.toString.call('xz'); //"[object String]"
 Object.prototype.toString.call(123);  //"[object Number]"
 Object.prototype.toString.call(true); //"[object Boolean]"
 Object.prototype.toString.call([1,2]); //"[object Array]"
 Object.prototype.toString.call({name:'xz'}); //"[object Object]"
 Object.prototype.toString.call(function(){}); //"[object Function]"
 Object.prototype.toString.call(null); //"[object Null]"
 Object.prototype.toString.call(undefined); //"[object Undefined]"
 Object.prototype.toString.call(); //"[object Undefined]"
 Object.prototype.toString.call(new Date()); //"[object Date]"
 Object.prototype.toString.call(/xz/);  //"[object RegExp]"
 Object.prototype.toString.call(Symbol()); //"[object Symbol]"

 var obj = {name:"Xzavier", age:23};
 var a = [1,2,3];

 function isType(obj) {
     return Object.prototype.toString.call(obj).slice(8, -1);
 }
 isType(obj);  // "Object" 
 isType(a)  // "Array"  

执行上下文与作用域

执行上下文

执行上下文分为:全局上下文函数上下文块级上下文。   变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上(基本无法通过代码访问这个变量,但是后台数据会处理到)。

  每个函数调用、代码块(if{}for等)执行都会有自己的上下文。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。上下文中的代码在执行的时候,会创建变量对象的一个作用域链(类似原型链的链接)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量: arguments

  代码正在执行的上下文的变量对象始终位于作用域链的最前端。全局上下文的变量对象始终位于作用域链的最后一个变量。 代码执行示的标识符解析(类似原型链只能一级一级往上找):先从作用域链最前端(即自身上下文)中查找,然后再逐级向上,最后一级为全局上下文。直到找到标识符为止,若没有找到则会报错

var color = "blue"; 
function changeColor() { 
 let anotherColor = "red"; 
 function swapColors() { 
 let tempColor = anotherColor; 
 anotherColor = color; 
 color = tempColor; 
 // 这里可以访问 color、anotherColor 和 tempColor 
 } 
 // 这里可以访问 color 和 anotherColor,但访问不到 tempColor 
 swapColors(); 
} 
// 这里只能访问 color 
changeColor();

  以上涉及三个上下文:全局上下文changeColor()的局部上下文swapColors()的局部上下文。每个上下文有个字的变量对象。下一级上下文可以访问上一级上下文中的变量对象。例子:changeColor()局部上下文不可以访问tempColor,而在swapColor()中则可以访问changeColor()上下文中的anotherColor变量

作用域链增强

变量声明

  变量是用于保存任意值的命名占位符

  • var(函数作用域)
    • 声明作用域: 用var声明的变量会成为包含它的函数的局部变量,若在function变量没有用任何声明关键字进行声明 则会变为全局变量。函数中的变量: 必须调用一次该函数才会定义函数内部的变量
    • 变量提升
      • 变量的声明分为: 定义声明(var a)赋值声明(a = 2),定义声明部分在js编译阶段会被提升到当前作用域的顶部,赋值声明和其他逻辑会停留在本地
      • 函数提升:函数声明会被提升,函数表达式不会被提升(前部分的定义声明会提升,但是函数赋值部分留在本地)
      • 优先级: 函数声明提升优先级比变量声明提升优先级更高,如果有多个相同的声明,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明
/**  var 声明作用域 */
function varFunc () {
  var localName = '我是一个局部变量';
  globalName = '我是一个全局变量';
}
varFunc(); // 函数执行后 函数内部的全局变量才会挂载到window
console.log(localName); // Reference Error:引用错误, localName是一个局部变量
console.log(globalName, window);

/** var 声明提升 */
console.log('输出在声明以及赋值之前', message); // undefined
var message = '变量提升';
console.log('输出在声明以及赋值后', message); // 变量提升
// 经典问题
for (var i = 0; i < 5; i++) {
  console.log('forr', i);
}
console.log(i); // 5
// 等价代码 - 变量提升
// var i;
// for (i = 0; i < 5; i++) {
//   console.log('forr', i);
// }
// console.log(i); // 5

/** 优先级 */
console.log(variableHoisting); // function variableHoisting()
function variableHoisting () {
  console.log('函数提升优先');
}
var variableHoisting = '变量提升';
var variableHoisting = function hh () { // 函数表达式不会提升
  console.log('hhhh');
}
console.log(variableHoisting); // function hh()  变量赋值:变量赋值位于最后执行阶段

// // 等价代码
// function variableHoisting () {
//   console.log('函数提升优先');
// }
// var variableHoisting; // 赋值为undefined = JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明,函数优先级更高
// console.log(variableHoisting);
// variableHoisting = '变量提升';
// console.log(variableHoisting); // 变量赋值: 变量赋值位于最后执行阶段
  • let(块作用域)
    • 暂时性死区 在 let 声明之前的执行瞬间被称为“暂时性死区”,在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。let声明的变量不会在作用域中被提升,let不能重复声明一个变量
    • 全局声明 即使 let是在全局作用域声明的变量也不会成为window对象的属性
    • 条件声明 let声明的变量不可以重复声明,js引擎不会自动合并声明
    • for 循环中的使用 let声明迭代变量时,JavaScript引擎在后台会为每一个迭代循环声明一个新的迭代变量
  var a; 
  var a; 
  // 不会报错
  { 
    let b; 
    let b; 
  } 
  // SyntaxError: 标识符 b 已经声明过了
  • const(块作用域)
    • const的行为与let基本相同,重要区别: const声明变量时必须同时初始化变量,且const声明的变量不可以进行修改
    • const声明的限制 只使用于指向它的变量的引用, 若const变量引用的是一个对象 则可修改当前变量内部的属性
  const a; // SyntaxError: 常量声明时没有初始化
  const b = 3; 
  console.log(b); // 3 
  b = 4; // TypeError: 给常量赋值

垃圾回收

标记清理

引用计数

内存管理