你不懂的js读书笔记

2020-09-17  本文已影响0人  潇潇潇潇潇潇潇

入门与进阶

第一章 进入编程

1,如何快速运行一段js代码?

方式1:浏览器开发者工具。可以使用快捷键option+command+J或者菜单选项开发者工具来启动开发者控制台。单选直接输入,多行按shift+enter切换行

方式2:Chrome的snippets是小脚本,还可以创作并在Chrome DevTools的来源面板中执行。 首先通过f12 打开开发者工具,再打开Sources面板中,单击上Snippets选项卡,在导航器中单击鼠标右键,然后选择New。

方式3:node脚本。运行node xxx.js,即可执行该js文件

方式4:在线js编辑器。https://codepen.io/ https://jsfiddle.net/

方式5:建demo项目,这种方式可以支持第三方库

2,常量命名习惯

//常量通常是大写的,在多个单词之间使用下划线_连接
const TAX_RATE = 0.08;

1、JS严格区分大小写,test和Test是两个变量。
2、命名遵循`驼峰命名法。
3、可以使用数字、字母、下划线、$来命名,但是数字不能作为名字的开始,也不支持中杠(-)
4、_开头的变量是公共变量(全局变量)(var _student)
5、常量通常是大写的,在多个单词之间使用下划线_连接
6、不能使用关键字和保留字命名;
    关键字:在JS中有特殊含义的,例如:var、for、break、continue…
    保留字:未来可能会成为关键字的,例如:class

第二章 进入JavaScript

1,如何比较两个值

==在允许强制转换的条件下检查值的等价性,而===是在不允许强制转换的条件下检查值的等价性;因此===常被称为“严格等价”。

var a = "42";
var b = 42;

a == b;         // true
a === b;        // false

即 == 先将两者强制转换为同一种类型再进行比较。

如果你在比较两个非基本类型值,比如object(包括function和array),它比较的是引用是否相同,而不是它们底层的值。

var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";

a == c;     // true
b == c;     // true
a == b;     // false

如果是不等价比较:<,>,<=,和≥,比较的两个值也是先强制转换,它使用典型的字母顺序规则("bar" < "foo",如下,

var a = 41;
var b = "42";
var c = "43";

a < b;      // true
b < c;      // true

需要注意的是,如果一个值是数字,另一个强制转换为数字,如下,值b被强制转换为了“非法的数字值”NaN

var a = 42;
var b = "foo";

a < b;      // false
a > b;      // false
a == b;     // false

总结一下JS中经常遇到纯数字和各种各样的字符串进行比较:

alert(1<3);//true

alert("1"<"3");//true

alert("123"<"123");//false

alert("a"<"b");//true

alert("abc"<"aad");//false,多纯字母比较,会依次比较ascii码

alert("我".charCodeAt());//25105

alert("的".charCodeAt());//30340

alert("我"<"的");//true,汉字比较,转成ascii码

alert(123<"124");//true,下面一句代码得出124的ascii码为49,所以并不是转成ascii比较

alert("124".charCodeAt());//49

alert(13>"abc");//false

作用域与闭包

1,需要弃用 var 改用 let 么?

是的,在 ES6 不要使用 var,用 let 或 const 代替。

https://www.zhihu.com/question/34294629

后续会增加js检验规则,全面禁用var

第一章 什么是作用域?

1,什么是作用域

它是一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量。换句话说,变量被存储在哪儿?而且,最重要的是,我们的程序如何在需要它们的时候找到它们

2,js 编译过程是怎样的?

编译程序一般步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。

分词/词法分析(Tokenizing/Lexing)。就好比我们将一句话,按照词语的最小单位进行分割。例如,考虑程序 var a=2。这段程序通常会被分解成为下面这些词法单元:var,a,=,2;空格是否作为当为词法单位,取决于空格在这门语言中是否具有意义。

解析/语法分析(Parsing)。将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树称为“抽象语法树”(Abstract Syntax Tree,AST)。词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,每取得一个词法记号,就将其送入语法分析器进行分析。举例说明

if (typeof a == "undefined") {
  a = 0;
} else {
  a = a;
}
alert(a);
image

代码生成。将 AST 转换成可执行代码的过程被称为代码生成。这个过程与语言、目标平台相关。

推荐另一篇文章,介绍的很详细。https://juejin.im/post/5c2ca1106fb9a049ac794510

推荐一个动手项目,自己写一个js解释器:https://juejin.im/entry/5c0538245188257c3045ccc3

3,ReferenceError和TypeError分别表示什么错误?

调用一个“未声明”的变量,在作用域中找不到,会报 ReferenceError。

变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError。

第二章 词法作用域

1,查询变量作用域的过程是怎样的

首先从最内部的作用域开始,如果找不到,则向上走一层,到外层最近的作用域。一旦找到第一个匹配的,查询就停止了。

image

在上面的代码段中,引擎 执行语句 console.log(..) 并开始查找三个被引用的变量 a,b 和 c。它首先从最内部的作用域气泡开始,也就是 bar(..) 函数的作用域。在这里它找不到 a,所以它向上走一层,到外面下一个最近的作用域气泡,foo(..) 的作用域。它在这里找到了 a,于是它就使用这个 a。同样的事情也发生在 b 身上。但是对于 c,它在 bar(..) 内部就找到了。

2,怎样欺骗词法作用域?

eval(..)、setTimeout(..)、setInterval(..)、with,都可以

以eval为例说明,eval(..) 函数接收一个字符串作为参数值,运行时动态翻译成执行代码

function foo(str, a) {
    eval( str ); // 作弊!
    console.log( a, b );
}

var b = 2;

foo( "var b = 3;", 1 ); // 1 3

eval(..) 和 with 都可以欺骗编写时定义的词法作用域,代价是让代码优化失去作用,影响代码执行速度

第三章 函数与块儿作用域

1,什么函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。

function foo(a) {
    var b = 2;

    // 一些代码

    function bar() {
        // ...
    }

    // 更多代码

    var c = 3;
}
bar(); // 失败

console.log( a, b, c ); // 3个都失败

2,什么是块作用域

块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. }内部)。

3,let关键字的作用

let关键字(var关键字的表亲),用来在任意代码块中声明变量。if (..) { let a = 2; }会声明一个劫持了if的{ .. }块的变量,并且将变量添加到这个块中

第4章 提升

1,为什么作用域会提升

变量和函数在内的所有声明都会在任何代码被执行前首先被处理,换句话说声明本身会被提升,而赋值或其他运行逻辑会留在原地。

a = 2;

var a;

console.log( a );// 2
//函数声明会被提升,就像我们看到的。但是函数表达式不会。
foo();

function foo() {
    console.log( a ); // undefined

    var a = 2;
}

尽量避免在同一个作用域中重复定义,经常会导致各种奇怪的问题

第5章 闭包

1,什么是闭包

闭包是指有权访问另一个函数作用域中的变量的函数。闭包保存了外部函数的作用域链中的变量对象,本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

闭包介绍:https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

2,js中的闭包跟ios中的闭包/block是同一个东西吗

不是,ios中的闭包是一个回调函数,而js中的闭包是能够读取其他函数内部变量的函数,通常用于封装私有变量,将子函数中的私有变量提供出来,让外层函数能够访问

// swift 闭包
outText(callback: { (text: String) in
    print(text)
})
// 对应的js回调函数
outText((text) => {
    console.log(text)
})

this与对象原型

第1章 关于this

1,js中this的原理

this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

https://www.ruanyifeng.com/blog/2018/06/javascript-this.html

2,为什么要用this

this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

第2章 this全面解析

1,this绑定优先级

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

1.函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

var bar = new foo()

2.函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

var bar = foo.call(obj2)

3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

var bar = obj1.foo()

4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

var bar = foo()

就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。

2,箭头函数有什么用

箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。

第3章 对象

1,什么是对象,为什么我们需要指向它们?

对象就是键/值对的集合。它是六种基本类型之一,对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]表示这是对象的子类型数组。

2,内置对象与对象有什么关系?

内置对象是一些对象子类型,如下

· String· Number· Boolean· Object· Function· Array· Date· RegExp· Error

这些内置函数可以当作构造函数(由new产生的函数调用——参见第2章)来使用,从而可以构造一个对应子类型的新对象。举例来说:

var strPrimitive = "I am a string";
typeof strPrimitive;                            // "string"
strPrimitive instanceof String;                 // false

var strObject = new String( "I am a string" );
typeof strObject;                               // "object"
strObject instanceof String;                    // true

// 考察 object 子类型
Object.prototype.toString.call( strObject );    // [object String]

我们在字符串的基本类型上调用属性和方法,引擎会自动地将它强制转换为 String 对象。

基本类型值 "I am a string" 不是一个对象,它是一个不可变的基本字面值。为了对它进行操作,比如检查它的长度,访问它的各个独立字符内容等等,都需要一个 String 对象。

JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。

3,如何动态访问对象内容

操作符要求属性名满足标识符的命名规范,而[".."]语法可以接受任意UTF-8/Unicode字符串作为属性名。举例来说,如果要引用名称为"Super-Fun! "的属性,那就必须使用["Super-Fun! "]语法访问,因为Super-Fun!并不是一个有效的标识符属性名。此外,由于[".."]语法使用字符串来访问属性,所以可以在程序中构造这个字符串,比如说:

var wantA = true;
var myObject = {
    a: 2
};

var idx;

if (wantA) {
    idx = "a";
}

// 稍后

console.log( myObject[idx] ); // 2

4,js 中有几种遍历方法,他们有什么区别?

for 语句、forEach语句、for-in 语句、for-of 语句 (ES 6)

for 语句是标准的循环写法,定义一个变量i作为索引,以跟踪访问的位置,len是数组的长度,条件就是i不能超过len。

forEach 方法对数组的每个元素执行一次提供的CALLBACK函数,forEach是一个数组方法,可以用来把一个函数套用在一个数组中的每个元素上,forEach为每个数组元素执行callback函数只可用于数组

var arr = [1,5,8,9]
arr.forEach(function(item) {
    console.log(item);
})

一般会使用for-in来遍历对象的属性的,不过属性需要 enumerable,才能被读取到. for-in 循环只遍历可枚举属性。

var obj = {
    name: 'test',
    color: 'red',
    day: 'sunday',
    number: 5
}
for (var key in obj) {
    console.log(obj[key])
}

for-of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。只要是一个iterable的对象,就可以通过for-of来迭代.

var arr = [{name:'bb'},5,'test']
for (item of arr) {
    console.log(item)
}

性能对比,如下

for > for-of > forEach > filter > map > for-in

详细资料:https://juejin.im/post/5a3a59e7518825698e72376b#heading-14

第4章 混合对象“类”

1,JS是面向对象编程还是面向过程编程?JS拥有类吗?

JS 虽然有近似类的语法,但是JavaScript的机制似乎一直在阻止你使用类设计模式。在近似类的表象之下,JavaScript的机制其实和类完全不同。语法糖和(广泛使用的)JavaScript“类”库试图掩盖这个现实,但是你迟早会面对它:其他语言中的类和JavaScript中的“类”并不一样。

2,为什么会有"类"设计模式的出现?

类是一种设计模式,类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。

3,js 中的继承、多态、多重继承

在继承或者实例化时,JavaScript的对象机制并不会自动执行复制行为。简单来说,JavaScript中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来(参见第5章)。由于在其他语言中类表现出来的都是复制行为,因此JavaScript开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。

// 大幅简化的 `mixin(..)` 示例:
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        // 仅拷贝非既存内容
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }

    return targetObj;
}

var Vehicle = {
    engines: 1,

    ignition: function() {
        console.log( "Turning on my engine." );
    },

    drive: function() {
        this.ignition();
        console.log( "Steering and moving forward!" );
    }
};

var Car = mixin( Vehicle, {
    wheels: 4,

    drive: function() {
        Vehicle.drive.call( this );
        console.log( "Rolling on all " + this.wheels + " wheels!" );
    }
} );

Car 现在拥有了一份从 Vehicle 得到的属性和函数的拷贝。技术上讲,函数实际上没有被复制,而是指向函数的 引用 被复制了。所以,Car 现在有一个称为 ignition 的属性,它是一个 ignition() 函数引用的拷贝;而且它还有一个称为 engines 的属性,持有从 Vehicle 拷贝来的值 1。Car已经 有了 drive 属性(函数),所以这个属性引用没有被覆盖(参见上面 mixin(..) 的 if 语句)。

类意味着拷贝。

当一个传统的类被实例化时,就发生了类的行为向实例中拷贝。当类被继承时,也发生父类的行为向子类的拷贝。

多态(在继承链的不同层级上拥有同名的不同函数)也许看起来意味着一个从子类回到父类的相对引用链接,但是它仍然只是拷贝行为的结果。

JavaScript 不会自动地 (像类那样)在对象间创建拷贝。

mixin 模式常用于在 某种程度上 模拟类的拷贝行为,但是这通常导致像显式假想多态那样(OtherObj.methodName.call(this, ...))难看而且脆弱的语法,这样的语法又常导致更难懂和更难维护的代码。

明确的 mixin 和类 拷贝 又不完全相同,因为对象(和函数!)仅仅是共享的引用被复制,不是对象/函数自身被复制。不注意这样的微小之处通常是各种陷阱的根源。

第5章 原型

参考资料:https://mp.weixin.qq.com/s/fMvSims4VBeoKs0JJhJYtA

  1. JS Prototype 原型对应的数据结构和算法是什么?

    JS 原型其实是一个隐式的单向链表。prototype 除了不叫 next,以及是一个隐式引用外,跟下面的单向链表结构如出一辙。

image
image
  1. 什么是 js 原型

    从数据结构的角度看,js 原型是一个以隐式引用作为存储方式,以点操作符和属性访问语句作为语法糖的单向链表。并且,原型链并没有发挥出单向链表的全部能力。大部分情况下,只用到了 addFirst 这个操作(即原型继承)。极少场景使用 addLast, traversing, insertBefore, insertAfter 等链表操作。

    JS 原型是指为其它对象提供共享属性访问的对象。在创建对象时,每个对象都包含一个隐式引用指向它的原型对象或者 null。

  2. 什么是原型链

    原型也是对象,因此它也有自己的原型。如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。

  3. js 中的 new 是什么

    那么在javaScript中new到底做了什么呢?其实当执行 var o = new Foo(); 时,javaScript实际执行如下(或类似):

    var o = new Object();
    o.__proto__ = Foo.prototype;
    Foo.call(o);
    //原型链为:
    // o ---> Function.prototype ---> Object.prototype ---> null
    

    所以new是什么?它实际上就是新生成一个对象o,然后把这个对象的prototype链到了Foo.prototype对象上。

  4. Object.setPropertyOf 和 Object.create 的差别

    1)Object.setPropertyOf,给我两个对象,我把其中一个设置为另一个的原型。

    2)Object.create,给我一个对象,它将作为我创建的新对象的原型。

  5. 类和原型的区别

    基于 class 的继承,继承的是行为和结构,但没有继承数据。

    而基于 prototype 的继承,可以继承数据、结构和行为三者。

  6. js 原型有哪些问题

    难以理清我访问的属性和方法,来自原型链的哪一个对象。我们需要手动判断 key 是否属于 obj 自身,然后进行真正的操作。因此,有一些开发者,建议不用 for in,总是使用 Object.keys。Object.keys 将 obj 自身包含的所有可遍历的 key,装配成数组形式返回。

    此外在原型上追加数据和方法,会影响到所有继承该原型的对象。如挂载到 Object, Array, Number 等全局构造函数的原型上,将使所有代码都变得更不可靠。

第6章 行为委托

  1. instanceof有什么问题,有没有替代方法

    使用Bar instanceof Foo(因为很容易把“实例”理解成“继承”),但是在JavaScript中这是行不通的,你必须使用Bar.prototype instanceof Foo。

    我们不再使用 instanceof,因为它令人迷惑地假装与类有关系。现在,我们只需要(非正式地)问这个问题,“你是我的 一个 原型吗?”。不再需要用 Foo.prototype 或者痛苦冗长的 Foo.prototype.isPrototypeOf(..) 来间接地查询了。

    // `Foo` 和 `Bar` 互相的联系
    Foo.isPrototypeOf( Bar ); // true
    Object.getPrototypeOf( Bar ) === Foo; // true
    
    // `b1` 与 `Foo` 和 `Bar` 的联系
    Foo.isPrototypeOf( b1 ); // true
    Bar.isPrototypeOf( b1 ); // true
    Object.getPrototypeOf( b1 ) === Bar; // true
    

类型和语法

第1章 类型

  1. null 检测

    null 有个存在20年的bug,如果要检测 null,可以使用复合条件

    var a = null;
    
    (!a && typeof a === "object"); // true
    
  2. 什么是undefined

    变量在未持有值的时候为undefined。此时typeof返回"undefined"

    var a;
    
    typeof a; // "undefined"
    
    var b = 42;
    var c;
    
    // 稍后
    b = c;
    
    typeof b; // "undefined"
    typeof c; // "undefined"
    

第2章 值

  1. 数组注意事项

    若有空缺单元时要注意,会默认填充一个undefined的值

    var a = [ ];
    
    a[0] = 1;
    // 这里没有设置值槽 `a[1]`
    a[2] = [ 3 ];
    
    a[1];       // undefined
    
    a.length;   // 3
    

    数组是对象的一种,因此也包含字符串键值和属性(但这些并不计算在数组长度内),最好不要当对象来使用。

    如果字符串键值是个数字,会被当成数字索引处理

    var a = [ ];
    
    a["13"] = 42;
    
    a.length; // 14
    
  2. 数字注意事项

    JavaScript没有真正意义上的整数,JavaScript中的“整数”就是没有小数的十进制数。所以42.0即等同于“整数”42。

    在处理带有小数的数字时需要特别注意。很多(也许是绝大多数)程序只需要处理整数,最大不超过百万或者万亿,此时使用JavaScript的数字类型是绝对安全的

    0.1 + 0.2 === 0.3; // false
    

    小数点解决方法:把小数转成整数后再运算

    https://github.com/camsong/blog/issues/9

    有时JavaScript程序需要处理一些比较大的数字,如数据库中的64位ID等。由于JavaScript的数字类型无法精确呈现64位数值,所以必须将它们保存(转换)为字符串。

    整数检测

    Number.isInteger( 42 );     // true
    Number.isInteger( 42.000 ); // true
    Number.isInteger( 42.3 );   // false
    
  3. 特殊数值注意事项

    undefined类型只有一个值,即undefined。null类型也只有一个值,即null。它们的名称既是类型也是值。

    undefined和null常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:

    • null指空值(empty value)

    • undefined指没有值(missing value)

    或者:

    • undefined指从未赋值

    • null指曾赋过值,但是目前没有值

    null是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而undefined却是一个标识符,可以被当作变量来使用和赋值。

  4. 特殊的数字注意事项

    NaN意指“不是一个数字”(not a number)

    var a = 2 / "foo";      // NaN
    
    typeof a === "number";  // true
    

    NaN是一个特殊值,它和自身不相等,可以使用ES6中的工具函数Number.isNaN(..)来判断一个值是否是NaN。

    var a = 2 / "foo";
    
    a == NaN;   // false
    a === NaN;  // false
    
    Number.isNaN( a ); // true
    Number.isNaN( 'foo' ); // true
    

    负零的检测

    function isNegZero(n) {
        n = Number( n );
        return (n === 0) && (1 / n === -Infinity);
    }
    
    isNegZero( -0 );        // true
    isNegZero( 0 / -3 );    // true
    isNegZero( 0 );         // false
    

    ES6中新加入了一个工具方法Object.is(..)来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:

    var a = 2 / "foo";
    var b = -3 * 0;
    
    Object.is( a, NaN );    // true
    Object.is( b, -0 );     // true
    
    Object.is( b, 0 );      // false
    

    Object.is 详细资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is

    关于 == vs === vs Object.is,结论是这三个比较运算符都非常有用,当进行一些普遍的比较时,则推荐使用 ===,而不是 ==;当遇到 NaN、0、+0、-0较为特殊值比较时,推荐使用Object.is。

  5. 值和引用注意事项

    简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。

    复合值(compound value)——对象(包括数组和封装对象,参见第3章)和函数,则总是通过引用复制的方式来赋值/传递。

    var a = 2;
    var b = a; // `b` 总是 `a` 中的值的拷贝
    b++;
    a; // 2
    b; // 3
    
    var c = [1,2,3];
    var d = c; // `d` 是共享值 `[1,2,3]` 的引用
    d.push( 4 );
    c; // [1,2,3,4]
    d; // [1,2,3,4]
    

    JavaScript中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值。

    var a = [1,2,3];
    var b = a;
    a; // [1,2,3]
    b; // [1,2,3]
    
    // 稍后
    b = [4,5,6];
    a; // [1,2,3]
    b; // [4,5,6]
    

    JavaScript中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值

    1.
    function foo(x){ //
        x.push(4);
        x; //(4) [1, 2, 3, 4]
    
          
            x = [4,5,6]; //x =[4,5,6] 该赋值并不影响a的指向,所以a仍然指向[1,2,3,4]
        x.push(7); 
            x;//(4) [4, 5, 6, 7]
    }
    
    var a = [1,2,3];
    foo(a);
    // a; //(4) [1, 2, 3, 4]
    
    2. 
    
    function foo(x){
        x.push(4);
          x; //(4) [1, 2, 3, 4]
    
        x.length = 0; //不能通过引用x来更改引用a的指向,只能更改a和x共同指向的值,
        x.push(4,5,6,7);
        x;  //(4) [4, 5, 6, 7]
    }
    
    var a = [1,2,3];
    foo(a);
    

第3章 原生函数

  1. js中的封装对象包装

    JavaScript会自动为基本类型值包装(box或者wrap)一个封装对象,比如"abc",如果要访问它的length属性或String.prototype方法,JavaScript引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问:

    var a = "abc";
    
    a.length; // 3
    a.toUpperCase(); // "ABC"
    

    一般情况下,我们不需要直接使用封装对象。最好的办法是让JavaScript引擎自己决定什么时候应该使用封装对象。即应该优先考虑使用"abc"和42这样的基本类型值,而非new String("abc")和new Number(42)。

    Date(..)和Error(..),没有对应的常量形式来作为它们的替代。

    拆封,valueOf(): 得到封装对象中的基本类型值

    var a = new String("a")
    var b = new Number(43)
    var c = new Boolean(true)
    
    a.valueOf() //"a"
    b.valueOf() //43
    c.valueOf() //true
    

第4章 强制类型转换

  1. toString()

    基本类型值的字符串化规则为:null转换为"null", undefined转换为"undefined", true转换为"true"。数字的字符串化则遵循通用规则,极小和极大的数字使用指数形式.

    对普通对象来说,除非自行定义,否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值(参见第3章),如"[object Object]"。

    数组在做字符串化时,将数组所有元素字符串化再用","连接。

    工具函数JSON.stringify(..)在将JSON对象序列化为字符串时也用到了ToString。JSON.stringify(..)在对象中遇到undefined、function和symbol时会自动将其忽略,在数组中则会返回null(以保证单元位置不变)。

  2. toNumber()

    将非数字值当作数字来使用,比如数学运算。其中true转换为1, false转换为0。undefined转换为NaN, null转换为0。处理失败时返回NaN。

    parseInt() 和 parseFloat()

    console.log(parseInt('a')) // NaN
    console.log(parseInt('11')) // 11
    console.log(parseInt('11aa')) // 11
    console.log(parseInt('0xf')) // 15
    
    console.log(parseFloat('12.3a')) // 12.3
    console.log(parseFloat('0xf')) // 0
    console.log(parseFloat('01.1')) // 1.1
    
  3. toBoolean

    console.log(Boolean("0")) // true
    console.log(Boolean([])) // true
    console.log(Boolean(undefined)) // false
    console.log(Boolean(null)) // false
    console.log(!!"0") // true
    console.log(!![]) // true
    console.log(!!undefined) // false
    console.log(!!null)  // false
    
  4. 日期显示转换为数字

    一元运算符+的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳,以毫秒为单位(从1970年1月1日00:00:00 UTC到当前时间):

    不建议对日期类型使用强制类型转换,推荐用Date.now();

    var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
    
    +d; // 1408369986000
    
    或者
    
    var timestamp = +new Date();
    
    或者 
    
    var timestamp = new Date().getTime();
    
    或者(推荐)
    
    var timestamp = Date.now();
    
  5. 显式转换为布尔值

    建议使用Boolean(a)和!! a来进行显式强制类型转换。

    var a = "0";
    var b = [];
    var c = {};
    
    var d = "";
    var e = 0;
    var f = null;
    var g;
    
    !!a;    // true
    !!b;    // true
    !!c;    // true
    
    !!d;    // false
    !!e;    // false
    !!f;    // false
    !!g;    // false
    
    Boolean( a ); // true
    Boolean( b ); // true
    Boolean( c ); // true
    
    Boolean( d ); // false
    Boolean( e ); // false
    
    Boolean( f ); // false
    Boolean( g ); // false
    

    || 和 &&

    和其他语言不同,在JavaScript中它们返回的并不是布尔值。

    它们的返回值是两个操作数中的一个(且仅一个),即选择两个操作数中的一个,然后返回它的值。

    var a = 42;
    var b = "abc";
    var c = null;
    
    a || b; //42
    a && b; //"abc"
    c || b; //"abc"
    c && b; //null
    

异步和性能

第1章 异步

  1. 分块的程序

    实际上,JavaScript程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。

    最常见的块单位是函数。现在无法完成的任务会以回调函数在将来完成。

    任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应等)时执行,这就是异步机制。

  2. 事件循环

    有一个用while循环实现的持续运行的循环,事件循环的每一轮称为一个 tick。 用户交互、IO 和定时器会向事件队列中加入事件。一旦有事件进入, 事件循环就会运行, 直到队列清空。任意时刻,一次只能从队列中处理一个事件。

    // `eventLoop`是一个像队列一样的数组(先进先出)
    var eventLoop = [ ];
    var event;
    
    // “永远”执行
    while (true) {
        // 执行一个"tick"
        if (eventLoop.length > 0) {
            // 在队列中取得下一个事件
            event = eventLoop.shift();
    
            // 现在执行下一个事件
            try {
                event();
            }
            catch (err) {
                reportError(err);
            }
        }
    }
    
  3. 并发

    js 一次只能处理一个事件,两个或多个进程同时执行就出现了并发。并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。

    如果进程间没有相互影响,不确定性是完全可以接受的。

    如果并发的进程需要相互交流,需要对它们进行协调以避免竞态的出现。如下

    var a, b;
    
    function foo(x) {
        a = x * 2;
        if (a && b) {
            baz();
        }
    }
    
    function bar(y) {
        b = y * 2;
        if (a && b) {
            baz();
        }
    }
    
    function baz() {
        console.log( a + b );
    }
    
    // ajax(..) 是某个包中任意的Ajax函数
    ajax( "http://some.url.1", foo );
    ajax( "http://some.url.2", bar );
    

    baz()调用周围的if (a && b)条件通常称为“大门”,因为我们不能确定a和b到来的顺序,但在打开大门(调用baz())之前我们等待它们全部到达。

  4. 任务

    任务队列,是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个项目(一个任务)。

    事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。

    console.log( "A" );
    
    setTimeout( function(){
        console.log( "B" );
    }, 0 );
    
    // 理论上的 "Job API"
    schedule( function(){
        console.log( "C" );
    
        schedule( function(){
            console.log( "D" );
        } );
    } );
    

    它将会打出A C D B,因为Job发生在当前的事件轮询tick的末尾,而定时器会在 下一个 事件轮询tick触发。

  5. 语句顺序

    代码中语句的顺序和JavaScript引擎执行语句的顺序并不一定要一致。编译器语句重排序几乎就是并发和交互的微型隐喻。

第2章 回调

回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。

第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。

我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。

第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方来调用你代码中的 continuation。 这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期、调用回调过早(在追踪之前)、 调用回调过晚(或没有调用)、调用回调的次数太少或太多(就像你遇到过的问题!)、没有把所需的环境/参数成功传给你的回调函数、吞掉可能出现的错误或异常。

第3章 Promise

如果不了解Promise,建议先看阮一峰的Promise介绍:docs.qq.com/doc/DSXlHUE9FRkVPVUNW

Promise 用于异步操作,表示一个还未完成但是预期会完成的操作。

  1. Promise 特点

    a. Promise对象的状态不受外界影响,pending 进行中状态、fulfilled 成功状态、rejected 失败状态,只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态

    b. Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆,只能由 pending变成fulfilled或者由pending变成rejected

  2. Promise 的用法

    a.Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署

    const promise = new Promise(function(resolve, reject) {
          // ... some code
    
          if (/* 异步操作成功 */){
            resolve(value);
          } else {
            reject(error);
          }
        });
    

    b. resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

    c. reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

    d. Promise 新建后就会立即执行。

    let promise = new Promise(function(resolve, reject) {
      console.log('Promise');
      resolve();
    });
    
    promise.then(function() {
      console.log('resolved.');
    });
    
    console.log('Hi!');
    
    // Promise
    // Hi!
    // resolved
    
  3. then

    then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

    then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    getJSON("/posts.json").then(function(json) {
      return json.post;
    }).then(function(post) {
      // ...
    });
    
  4. catch

    Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

    getJSON('/posts.json').then(function(posts) {
      // ...
    }).catch(function(error) {
      // 处理 getJSON 和 前一个回调函数运行时发生的错误
      console.log('发生错误!', error);
    });
    

    上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。

    Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

    getJSON('/post/1.json').then(function(post) {
      return getJSON(post.commentURL);
    }).then(function(comments) {
      // some code
    }).catch(function(error) {
      // 处理前面三个Promise产生的错误
    });
    

    如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

    const someAsyncThing = function() {
      return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
      });
    };
    
    someAsyncThing().then(function() {
      console.log('everything is great');
    });
    
    setTimeout(() => { console.log(123) }, 2000);
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123
    
  5. finally

    finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    
  6. all

    Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    const p = Promise.all([p1, p2, p3]);
    

    p的状态由p1、p2、p3决定,分成两种情况。当p1、p2、p3的状态都变成fulfilled时,p的状态才变成fulfiled,当p1、p2、p3之中任一个被rejected,这的状态就变成rejected

  7. race

    Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    const p = Promise.race([p1, p2, p3]);
    
  8. 什么是 Promise

    举例说明Promise概念

    设想一下这样一个场景:我走到快餐店的柜台,点了一个芝士汉堡。我交给收银员1.47美元。通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求。我已经启动了一次交易。但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号的收据。订单号就是一个IOU(I owe you,我欠你的)承诺(promise),保证了最终我会得到我的汉堡。
    我已经在想着未来的芝士汉堡了,尽管现在我还没有拿到手。我的大脑之所以可以这么做,是因为它已经把订单号当作芝士汉堡的占位符了。从本质上讲,这个占位符使得这个值不再依赖时间。这是一个未来值。
    终于,我听到服务员在喊“订单113”,然后愉快地拿着收据走到柜台,把收据交给收银员,换来了我的芝士汉堡。
    每次点芝士汉堡,我都知道最终要么得到一个芝士汉堡,要么得到一个汉堡包售罄的坏消息,那我就得找点别的当午饭了。

    假定执行一个任务,那么这个函数可能是立即完成也可能是需要一段时间才能完成。
    我们需要知道的是这个函数什么时候结束,这样我们就可以进行下一个任务了。
    因此我们需要实现监听,而监听执行结果的函数又两个,就是resolve和reject,这两个都是回调函数,resolve用于监听成功的结果(成功后执行),而reject则是失败,这就是resolve和reject的由来(知其所以然)

  9. 信任问题

    调用过早:根据定义,Promise就不必担心这种问题,因为即使是立即完成的Promise(类似于new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。

    调用过晚:一个Promise决议后,这个Promise上所有的通过then(..)注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。

    p.then( function(){
        p.then( function(){
            console.log( "C" );
        } );
        console.log( "A" );
    } );
    p.then( function(){
        console.log( "B" );
    } );
    // A B C
    

    回调未调用:如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。如果Promise本身永远不被决议,可以用使Promise超时的工具timeoutPromise

    调用次数过多或过少:如果出于某种原因,Promise创建代码试图调用resolve(..)或reject(..)多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议,并默默地忽略任何后续调用。

    未能传递参数/环境值:Promise至多只能有一个决议值(完成或拒绝)。如果你没有用任何值显式决议,那么这个值就是undefined。如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。

    吞掉错误或异常:如果在Promise出现了一个JavaScript异常错误,比如一个TypeError或ReferenceError,那这个异常就会被捕捉,并且会使这个Promise被拒绝。

  10. 链式流

    我们可以把多个 Promise 连接到一起表示一系列异步步骤。Promise 规范了异步,并封装了时间相关值的状态,因此我们可以把它们链式连接起来

    调用Promise的then(..)会自动创建一个新的Promise从调用返回。

    在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。

    如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)返回的链接Promise的决议值。

  11. 错误处理

    一些开发者宣称Promise链的“最佳实践”是,总是将你的链条以catch(..)终结,就像这样:

    var p = Promise.resolve( 42 );
    
    p.then(
        function fulfilled(msg){
            // 数字没有字符串方法,
            // 所以这里抛出一个错误
            console.log( msg.toLowerCase() );
        }
    )
    .catch( handleErrors );
    

    要是handleErrors(..)本身也有错误呢?谁来捕获它?

    你不能仅仅将另一个catch(..)贴在链条末尾,因为它也可能失败。Promise链的最后一步,无论它是什么,总有可能,即便这种可能性逐渐减少,悬挂着一个困在未被监听的Promise中的,未被捕获的错误。

  12. Promise 局限性

    Promise的设计局限性(具体来说,就是它们链接的方式)造成了一个让人很容易中招的陷阱,即Promise链中的错误很容易被无意中默默忽略掉。

    Promise 只能有一个单一值或一个拒绝理由

    Promise 只能决议一次(完成或拒绝)

    一旦创建了一个Promise并为其注册了完成和/或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。

    Promise 进行的动作要多一些,自然意味着它会稍慢一些

第4章 生成器

如果不熟悉生成器,推荐先了解下生成器的教程:https://es6.ruanyifeng.com/#docs/generator

第5章 性能

在这一章里,介绍了几种能够进一步提高性能的程序级别的机制。

第6章 性能测试与调优

ES6与未来

起步上路

ES6及更新版本

第1章 ES?现在与未来

第2章 语法

第3章 代码组织

第5章 集合

第6章 新增API

第7章 元编程

上一篇 下一篇

猜你喜欢

热点阅读