JavaScript高级程序设计读后感(一)之零碎知识点查漏补缺


目录
  • 1-script延迟脚本defer及异步脚本async,区别及应用场景
  • 2-未声明的变量,未初始化变量
  • 3-Number parseInt 字符串转数值 ,进制转换
  • 4-undefined && null 区别
  • 5-操作符
  • 6-Label语句
  • 7-with语句
  • 8-垃圾回收机制简述
    • 避免内存泄漏
  • 9-函数内部属性 arguments
  • 10-严格模式

1-script延迟脚本defer及异步脚本async,区别及应用场景

  1. defer和async在读取下载时是一样的,相对于html解析来说都是异步的。
  2. 区别是下载完后执行时间
  3. defer:立即下载,延迟执行。是最接近我们对脚本加载和执行要求的,要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。但最好只包含一个。
  4. async:立即下载,立即执行。不管声明顺序如何,只要加载完就会立即执行。用处不大,因为它完全不考虑依赖,不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的

defer:脚本代码依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖。

async:脚本并不关心页面中的DOM元素(文档是否解析完毕),并且也不会被其他脚本文件依赖。

2-未声明的变量,未初始化变量

  1. 未声明的变量,只能进行typeof操作符检测其数据类型,而delete操作本身不会导致错误,但并没有意义,且严格模式下会抛异常
  2. 未声明的变量赋值,在非严格模式下为全局变量,而严格模式下抛异常
  3. 未初始化变量执行typeof操作会返回undefined,而未声明的变量执行typeof操作同样会返回undefined

3-Number parseInt 字符串转数值 ,进制转换

Number:可以看出在字符串转数字时,能够识别十六进制和十进制,而在进制转换时,并不能很准确的识别是二进制还是八进制,因此提供了Number.toString(radix)方法。

// 转数值
Number('0x34')      // 52(十六进制数)
Number('0111')      // 111(十进制数)
Number('00001111')  // 1111(十进制数)
// 进制转换
Number(0x34)      // 52(十六进制数)
Number(0111)      // 73(八进制数)
Number(111)       // 111(十进制数)
Number(00001111)  // 585(八进制数)

22.toString(16)    // '16'(十六进制数)
22..toString(8)    // '26'(八进制数)
22..toString(2)    // '10110'(二进制数)

parseInt: 使用该方法解析二进制及八进制字符串时,ECMAScript3和5 存在分歧,

ECMAScript3中,'070'呗认为是八进制,因此转换后为十进制的56,

ECMAScript5中,该方法已经不具备解析八进制的能力,会忽略字符串的前导0,被解析为70,

为消除以上问题,该方法提供了第二个参数,即转化时使用的进制:

// 转数值 & 进制转换
parseInt('0x34')      // 52(十六进制数)
parseInt('00001111')  // 1111(十进制数)
parseInt('070')       // 70(十进制数)
parseInt(070)         // 56(八进制数)

parseInt('70',16)     // 112(十六进制数)
parseInt('70',10)     // 70(十进制数)
parseInt('70',8)      // 56(八进制数)
parseInt('70',2)       // NaN
parseInt('070',2)      // 0(二进制数)

4-undefined && null 区别

undefined null
转数值 NaN 0
typeof 'undefined' 'object'
if语句 被自动转为false 被自动转为false
应用场景 变量声明未赋值时,就等于undefined 一个表示"无"的对象
函数定义形参未传实参,该参数等于undefined 作为对象原型链的终点
对象还未赋值的属性,该属性的值为undefined
函数没有返回值时,默认返回undefined。

5-操作符

一元操作符: ++ / --

  • 前置递增递减,变量的值都是在包含它们的语句被求值之执行
  • 后置递增递减,变量的值都是在包含它们的语句被求值之执行

一元加和减操作符: + / -

  • 一元操作符以一个加号(+)表示,放在数值前面,对数值不会产生任何影响。放在非数值前,会像Number()转型函数一样进行转化。
  • 一元操作符以一个减号(-)表示,放在数值前面,该值会变成负数。放在非数值前,会像Number()转型函数一样进行转化。得到的数值转为负数。
let num1 = 2;
let num2 = 20;
let num3 = --num1 + num2;   // 21
//let num3 = num1-- + num2;   // 22
let num4 = num1 + num2;     // 21

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = { 
  valueOf() {
    return -1;
  }
};
           
s1 = -s1;  // -1
s2 = -s2;  // -1.1
s3 = -s3;  // NaN
b = -b;    // 0
f = -f;    // -1.1
o = -o;    // 1

6-Label语句

在 JavaScript 中,使用 label 语句可以为一行语句添加标签,以便在复杂结构中,设置跳转目标。语法格式:label : states

label 为任意合法的标识符,但不能使用保留字。然后使用冒号分隔签名与标签语句。

多与 break 语句配合使用,主要应用在循环结构、多分支结构中,或者嵌套的 switch 结构。且需要退出非当前层结构,以便跳出内层嵌套体。break label;,break 与label之间不能包含换行符,否则会解析为两个句子。如果没有设置标签名,则表示跳出当前最内层结构。

var num = 0;
outer: for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      break outer;
      // break ;
      // continue outer;    //  跳出本次循环,开始下一次循环,效果同break
    }
    num++;
  }
}
console.log('break label:' + num);    // 55
console.log('break:' + num);          // 95
console.log('continue label:' + num); // 95

7-with语句

用于设置代码在特定对象中的作用域,也就是将 statement 中的变量作用域添加到 expression 中。语法:with (expression) { statement }

with语句中查询变量顺序: 是否是 with语句中的局部变量 -> 是否是 expression中的变量 ->查找更高作用域范围

// eg1:
var sMessage = "hello";
function toUpperCase(){
  console.log('word');
}
with(sMessage) {
  console.log(toUpperCase());	 //输出 "HELLO"
}

// eg2:
 var obj = {
   a: 'aa',
   b: 'bb',
   c: 'cc',
 }
 with(obj) {
   var a = a
   var b = b
   var c = c
 }
 /**
 * 等同于
  var a = obj.a
  var b = obj.b
  var c = obj.c
 */

调用 toUpperCase() 时,解释程序将检查该方法是否是当前作用域函数。如果不是,将检查伪对象 sMessage,看它是否为该对象的方法。 输出 "HELLO",因为解释程序找到了字符串 "hello" 的 toUpperCase() 方法。

提示:with 语句是运行缓慢的代码块,会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用with语句 。

with语句会创建新的变量对象,起到延长作用域链的作用,另外try catch中的错误对象,也会被添加到变量对象中,起到延长作用域链的作用。

8-垃圾回收机制简述

垃圾回收:JS代码运行,需要操作系统或者运行时环境分配内存空间,来存储变量及它的值。当某些变量不在参与运行时,就需要系统回收被占用的内存空间,称为垃圾回收,而Javascript具有自动垃圾回收机制(GC:Garbage Collecation)

内存泄漏:不再用到的变量所占内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可导致系统崩溃、服务器宕机。

目前各大浏览器通常用采用的垃圾回收有两种方法:标记清除引用计数

  1. 标记清除

    • JS中最常用的垃圾回收方式。当变量进入执行环境时,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”
    • 垃圾收集器在运行时会给存储在内存中的所有变量都加上标记。然后去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
  2. 引用计数

    另一种不太常见的垃圾回收策略是引用计数,跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就+1。相反,如果该引用的变量又取得了另外一个值,则这个值的引用次数就-1。当引用次数为0时,则说明没有办法再访问到这个值,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。

    而引用计数有个最大的问题: 循环引用,如对象A有一个属性指向对象B,而对象B也有一个属性指向对象A。obj1和obj2通过各自的属性相互引用;也就是说这两个对象的引用次数都为2。

    在采用标记清除的策略时,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题。

    在采用引用计数的策略时,函数执行完成之后,obj1和obj2还将会继续存在,因为他们的引用次数永远不会是0。如果该函数被多次调用,就会导致大量的内存得不到回收。

function func() {
   let obj1 = {};
   let obj2 = {};
   
   obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

因此大部分浏览器都是以标记清除来实现垃圾回收机制,而IE老版本中有一部分对象并不是原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model,组件对象)对象的形式实现的,而COM对象的垃圾回收器就是采用的引用计数的策略。因此,即使IE的JS引擎使用标记清除的策略来实现的,但JS访问的COM对象依然是基于引用计数的策略的。换句话说,只要IE中涉及COM对象,就会存在循环引用的问题。

解决: 未避免, 最好是在不使用时,手动断开二者的引用关联,垃圾收集器下次运行时,就可以删除这些值并回收对应的内存

   // 循环引用
   var ele = document.getElementById('app');
   var myObj = {};
   myObj.node = ele;
   ele.style = myObj;
   // 手动断开二者的引用关联 
   myObj.node = null;
   ele.style = null;

IE9把DOM和BOM对象都转换成真正的JS对象,这样就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象。

避免内存泄漏

  1. 清空数组优化:arr = []虽然可以清空数组,同时又创建了一个新数组 ,而arr.length = 0也能达到清空数组的目的,同时能实现数组重用,减少内存垃圾的产生。
  2. 循环优化:在循环中的函数表达式,能复用最好放到循环外面。
  3. 意外的全局变量:局部变量未声明,会变成一个全局变量,在页面关闭之前不会被释放,若非必要尽量声明使用局部变量。
  4. 被遗忘的定时器和回调函数:定时器任务执行完毕后,需要清除定时器。
  5. 闭包:闭包可以维持函数内局部变量,使其得不到释放。

9-函数内部属性 arguments

虽然arguments的主要用途是保存函数的参数,但这个对象还有一个callee属性,该属性是一个指针,指向拥有这个arguments对象的函数,表示对函数对象本身的引用。

// 阶乘函数递归,使用arguments.callee 与函数名解耦
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
console.log(factorial(5));   // 120

// 使用新的引用替换阶乘方法, 仍可以使用原函数体,实现解耦
let trueFactorial = factorial;
                   
factorial = function() {
  return 0;
};
                   
console.log(trueFactorial(5));  // 120
console.log(factorial(5));      // 0

ECMAScript5规范化了另一个函数对象的属性,caller,该属性保存着调用当前函数的函数引用,如果是在全局作用域下调用当前函数,则它的值为null。

为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);              // [Function: outer]
  console.log(arguments.callee.caller);   // [Function: outer]
}
outer();    

10-严格模式

通过严格模式,可以在函数内部选择进行较为严格的全局或局部的错误条件检测,好处就是可以提早知道代码中存在的错误,及时捕获一些可能导致编程错误的ECMAScript行为

选择进入严格模式,可以使用严格模式的编译指示,实际就是一个不会赋给任何变量的字符串

“use strict”,如果你没有控制页面中所有脚本的权力,建议只在需要测试的特定函数中开启严格模式。

  • 未声明的变量赋值,str = "hello world",非严格模式会意外创建全局变量,严格模式抛出ReferenceError;

  • 删除变量,var str = 'hello';delete str;非严格模式会默认返回false,严格模式抛出ReferenceError;

  • 为对象的只读属性赋值会抛出TypeError;

  • 为对象不可配置的属性使用delete会抛出TypeError;

  • 为不可扩展的对象添加属性会抛出TypeError;

  • 函数的参数重名,非严格模式通过参数名只能访问第二个参数,要访问第一个参数需要使用arguments,严格模式抛出SyntaxError;

  • 修改命名参数的值,非严格模式下修改会映射到arguments中,严格模式下修改不会映射到arguments中

  function get(foo){ 
      "use strict";
      foo = 'bar'; 
      console.log(foo);   // bar
      console.log(arguments[0]);  // 非严格 bar  严格 Hi
  } 
  get('Hi')
  • 不支持八进制字面量,报错

  • 不允许使用with语句,视为语法错误

  • 不能访问arguments.callee,arguments.caller,不能为函数的caller属性赋值。

  • 在eval()中创建变量,在外部访问不到任何eval()中创建的任何变量或函数。

  function doSomething(){
  	eval("var x = 10");
  	console.log(x);  // 非严格 打印10 ,严格模式抛出 ReferenceError
  }
  • this指向问题,非严格模式下,使用函数的apply()或者call()方法时,null或undefined值会被转换为全局对象,而在严格模式下,函数的this值始终是指定的值,无论指定的是什么值
var color = 'red';
function displayColor(){
	console.log(this.color);
}
// 非严格访问全局打印 red, 
// 严格模式TypeError: Cannot read properties of null (reading 'color')
displayColor.call(null);  

相关