《js高级程序设计》笔记

@一棵菜菜  September 30, 2018

说明

打好js基础!整理在学习阅读红皮书《js高级程序设计》一书过程中的笔记。勤思考,多回顾。

第二章

把 JavaScript 插入到 HTML 页面中要使用<script>标签。两种方式:

  1. 使用这个标签可以把 JavaScript 嵌入到HTML 页面中,让脚本与标记混合在一起;
  2. 也可以包含外部的 JavaScript 文件。

而我们需要注意的地方有:

  • 所有<script>标签都会按照它们在页面中出现的先后顺序依次被解析 (顺序:从上到下) 。在不使用 defer 和 async 属性的情况下,只有在解析完前面<script>标签中的代码之后,才会开始解析后面<script>标签中的代码。
  • 由于浏览器会先解析完不使用 defer 属性的<script>标签中的代码,然后再解析后面的内容,所以一般应该把<script>标签放在页面最后,即主要内容后面,</body>标签前面。
  • 使用 defer 属性可以让脚本在文档完全呈现之后再执行。延迟脚本总是按照指定它们的顺序执行。
  • 使用 async 属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照它们在页面中出现的顺序执行。

第四章 变量、作用域

4.1 基本类型和引用类型的值

4.1.3 参数传递

ECMAScript 中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。

访问变量有按值和按引用两种方式,而参数只能按值传递。

函数参数是引用类型时

function setName(obj) {
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);    //"Nicholas"

可分析出作用域链:

window
 |—— person
 |—— setName()
   |——obj
代码分析
以上代码中创建一个对象,并将其保存在了变量 person 中。然后,这个变量被传递到 setName() 函数中之后就被复制给了 obj。在这个函数内部,obj 和 person 引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj 也会按引用来访问同一个对象。
于是,当在函数内部为 obj 添加 name 属性后,函数外部的 person 也将有所反映;因为 person 指向的对象在堆内存中只有一个,而且是全 局对象。有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明 参数是按引用传递的。

为了证明对象是按值传递的,我们再看一看下面这个经过修改的例子:

function setName(obj) { 
    obj.name = "Nicholas"; 
    obj = {}; 
    obj.name = "Greg";
}
var person = {};
setName(person);
alert(person.name); //"Nicholas"
代码分析
在把 person 传递给 setName()后,其 name 属性被设置为"Nicholas"。然后,又将一个新对象赋给变量 obj,同时将其 name 属性设置为"Greg"。如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值 为"Greg"的新对象。但是,当接下来再访问 person.name 时,显示的值仍然是"Nicholas"。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变
实际上,当在函数内部重写 obj 时,这 个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁

函数参数是基本数据类型时

function add(num) {
  num += 10;
  return num;
}

var num = 10;
console.log(add(num));
console.log(num);
window
 |—— num
 |—— add()
   |——num
结果:20 10
分析:add函数执行时,首先在自身的作用域内查找是否有定义变量num,结果找到了,所以就直接进行操作。

my结论

ECMAScript 中所有函数的参数都是按值传递的,但参数是引用类型时仍按引用来访问同一个对象。参数时基本数据类型时是向上查找作用域链。

4.1.4 检测类型

更多具体内容请查看我的文章《判断数组类型的终极大法(Array)》

typeof 操作符

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。

var b = true;
var u;
var o = new Object();
function test(){};

alert(typeof 'hello');// 'string'
alert(typeof 2);// 'number'
alert(typeof b);// 'boolean'
alert(typeof u);// 'undefined'

// 注意:null 不是 object
alert(typeof null);// 'object'

alert(typeof o);// 'object'
alert(typeof {});// 'object'
alert(typeof [1,2]); // 'object' 无法区别出来是否是数组类型

alert(typeof test);// function

instanceof 操作符

通常,我们并不是想知道某个值是对象,而是想知道它是什么类型的对象,可以使用instanceof所有引用类型的值都是 Object 的实例

result = variable instanceof constructor

如果变量是给定引用类型(根据它的原型链来识别)的实例,那么instanceof 操作符就会返回 true。请看下面的例子:

var person={},colors=['blue','yellow'];
alert(person instanceof Object); // 变量 person 是 Object 吗? 
alert(colors instanceof Array); // 变量 colors 是 Array 吗? 
alert(patter ninstanceof RegExp); //变量pattern是RegExp吗?

4.2 执行环境及作用域

此部分内容已单独成文,点击查看

4.2.1 延长作用域链

有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。当执行流进入下列任何一个语句时,作用域链就会得到加长:

  1. try-catch 语句的 catch 块
  2. with 语句

4.2.2 没有块级作用域

JavaScript 没有块级作用域(执行环境)!

a. if 语句

在 JavaScript 中,if 语句中的变量声明会将变量添加到当前的执行环境(在这里是 全局环境)中。

 if (true) {
        var color = "blue";
 }
 alert(color);    //"blue"

b. for 语句

JavaScript 来说,由 for 语句创建的变量 i 即使在 for 循环执行结束后,也依旧会存在 于循环外部的执行环境中。

 for (var i=0; i < 10; i++){
    doSomething(i);
 }
 alert(i);      //10

1. 声明变量

使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境。

建议在初始化变量之前,一定要先声明

2. 查询标识符【重点】

当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什 么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到 了该标识符,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域链向上 搜索。搜索过程将一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个标识符,则意味 着该变量尚未声明。

eg1:

var color = "blue";
  function getColor(){
      var color = "red";// 关键
      return color;
  }
  alert(getColor());  //"red"。

注意:如果没有var color = "red";这一步,则结果为"blue"。

eg2:

  var color = 'blue';
    function getColor(){
        var color = ‘red’;
        console.log(222,window.color)
        window.color = 'yellow';
        console.log(333,window.color)

        return color;
    }
    console.log(111,getColor());

    // 222 "blue"
    // 333 "yellow"
    // 111 "red”
任何位于局部变量 color 的声明之后的代码,如果不使用 window.color 都无法访问全局 color 变量。

4.3 垃圾收集【理解】

我的笔记《js常见的两种垃圾回收方法—引用计数和标记清除》

JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作

垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。
用于标识无用变量的策略可能会因实现而异,在浏览器中则通常有以下两个策略:

  1. 标记清除
  2. 引用计数

4.3.1 标记清除【理解原理 P79】

JavaScript 中最常用的垃圾收集方式。这种算法的思想是给当前不使用的值加上标记,然
后再回收其内存。

4.3.2 引用计数

这种算法的思想是跟踪记录所有值被引用的次数。但可能造成循环引用的问题!(JavaScript
引擎目前都不再使用这种算法)

为了避免循环引用问题,可以使用下面的代码消除前面例子创建的循环引用:

myObject.element = null;
element.someObject = null;
将变量设置为 null 意味着手动切断变量与它此前引用的值之间的连接。——解除引用

4.3.3 性能问题

垃圾收集器是周期性运行的。

在有的浏览器中可以触发垃圾收集过程,但我们不建议读者这样做。

4.3.4 管理内存

分配给 Web 浏览器的可用内存数量通常要比分配给桌面应用程序的少(这样做的目的主要是出于安全方面的考虑, 目的是防止运行 JavaScript 的网页耗尽全部系统内存而导致系统崩溃)。

解除引用【重点】

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用,

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
 }

var globalPerson = createPerson("Nicholas");
// 手工解除 globalPerson 的引用
globalPerson = null;

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。

第5章 引用类型

引用类型是一种数据结构,用于将数据和功能组织在一起。它们描述的是一类对象所具有的属性和方法。


5.1 Object类型

创建 Object 实例

1. new+构造函数

新对象是使用 new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。

 var person = new Object();

2. 对象字面量表示法

var person = {
    name : "Nicholas",
    "age" : 29,
     5 : true
};

注意:

  1. 在最后一个属性后面添加逗号,会在 IE7 及更早版本和Opera 中导致错误。
  2. 在使用对象字面量语法时,属性名也可以使用字符串。上面例子会创建一个对象,包含三个属性:name、age和5。但这里的数值属性名会自动转换为字符串
  3. 对象的属性名可以为字符串类型或者Symbol类型。

如果留空其花括号,则可以定义只包含默认属性和方法的对象,如下所示:

var person = {}; //与new Object()相同

使用对象字面量语法的好处:

  1. 要求的代码量少
  2. 能够给人封装数据的感觉
  3. 实际上,对象字面量也是向函数传递大量可选参数的首选方式(最好的做法是 对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数。如下:)
 function displayInfo(args) {
    var output = "";
    if (typeof args.name == "string"){
       output += "Name: " + args.name + "\n";
    }
    alert(output);
}
displayInfo({
    name: "Nicholas",
    age: 29
});

5.2 Array类型

数组的介绍和操作方法等已经单独成文,点击查看


5.3 Date类型

Date 类型使用自 UTC(Coordinated Universal Time,国际协调时间)1970 年 1 月 1 日午夜(零时)开始经过的毫秒数来保存日期

创建一个日期对象:

var date = new Date();// Fri Mar 01 2019 14:51:44 GMT+0800 (中国标准时间)
  • 在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。
  • 如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数。为了简化这一计算过程,ECMAScript 提供了两个返回相应日期的毫秒数方法:Date.parse()Date.UTC()

Date.UTC()

Date.UTC()new Date()的参数分别是:年份、基于 0 的月份(一月是 0,二月是 1,以此类推)、月中的哪一天 (1 到 31)、小时数(0 到 23)、分钟、秒以及毫秒数。在这些参数中,Date.UTC()里只有前两个参数(年和月)是必需的。如果没有提供月中的天数,则假设天数为1;如果省略其他参数,则统统假设为 0。

// GMT时间2000年1月1日午夜零时
var y2k = new Date(Date.UTC(2000,0))
// GMT时间2005年5月5日下午5:55:55
 9 var allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));
 
// 上面例子等同于:
// 本地时间2000年1月1日午夜零时
var y2k = new Date(2000, 0);// Sat Jan 01 2000 00:00:00 GMT+0800 (中国标准时间)
// 本地时间2005年5月5日下午5:55:55
var allFives = new Date(2005, 4, 5, 17, 55, 55);

以上代码创建了与前面例子中相同的两个日期对象,只不过下面的日期都是基于系统设置的本地时区创建的。

Date.now()

返回表示调用这个方法时的日期和时间的毫秒数

获取函数执行时间

//取得开始时间
var start = Date.now();// 1551434597281
//调用函数 
doSomething();
//取得停止时间
var stop = Date.now(),
result = stop – start;
支持 Data.now()方法的浏览器包括 IE9+、Firefox 3+、Safari 3+、Opera 10.5 和 Chrome。在不支持它的浏览器中,使用+操作符把 Data对象转换成字符串【常用】,也可以达到同样的目的。如下:
//取得开始时间
var start = +new Date();
//调用函数 
doSomething(); 
//取得停止时间
var stop = +new Date(),
result = stop - start;

5.3.3 日期/时间组件方法【了解,P102】

熟悉 Date 类型的方法列表

var date = new Date();
date.getTime();

5.5 Function类型

函数是对象,函数名是指针

每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定

function test(){}
typeof test === 'function' // true

定义函数的两种方式

  1. 使用函数声明语法定义

    function sum (num1, num2) {
        return num1 + num2;
    }
  2. 使用函数表达式定义

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

在使用函数表达式定义函数时注意:

  1. 没有必要使用函数名——通过变量 sum 即可以引用函数。
  2. 函数末尾有一个分号,就像声明其他变量时一样。

区别
函数声明与函数表达式的区别仅在于:什么时候可以通过变量访问函数(见下方「函数声明提升」)。

理解"函数名是指针"

由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字,如下面的例子所示。

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

alert(sum(10,10));        //20
var anotherSum = sum;
alert(anotherSum(10,10)); //20
// 将 sum 设置为 null,让它与函数“断绝关系”
sum = null;
alert(anotherSum(10,10)); //20

5.5.1 没有重载(深入理解,P111)

将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念。

5.5.2 函数声明与函数表达式

函数声明提升

解析器在向执行环境中加载数据时,解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

alert(sum(10,10));// 20
function sum(num1, num2){
    return num1 + num2;
}
分析:【先声明,再赋值
因为在代码开始执行之前,解析器就已经通过一个名为函数声明提升 (function declaration hoisting)的过程,读取并将函数声明添加到执行环境中(即使声明函数的代码在调用它的代码后面,JavaScript引擎也能把函数声明提升到所在执行环境的顶部)。
console.log(a);
console.log(a(2));
var a = function(x){
    return x*2
};

结果:undefinedTypeError: a is not a function

上面的代码在解析器实际执行为(先声明,再赋值)如下:

var a; // 先声明
console.log(a);
console.log(a(2));
a = function(x){return x*2}; // 再赋值

5.5.3 作为值的函数

可以从一个函数中返回另一个函数,而且这也是极为有用的一种技术。

// 比较函数(从小到大排列)
  function createComparisonFunction(propertyName) {
        return function(object1, object2){

添加新评论