速通JS Day1-7

· · 科技·工程

Learn From Scratch

开头使用"use strict"来使用ECMA5及以后的标准。 最好在每一个语句之后加上分号;。 使用let, const而非var!!!!!(let有高贵的块级作用域,没有声明提升 hoisting) HTML里面可以引用外部脚本文件(下载到缓存),但是标签需要闭合:

<script src="relative_or_absolute_path/script.js"></script>

数据类型:Number(±Infinity NaN)、BigInt±(253-1)、String(格式字符串的使用反引号${})、Boolean、null(无、空、值未知)、undefined(未被赋值)、Object、Symbol。可以强制类型转换。 数字转换:

变成……
undefined NaN
null 0
true / false 1 / 0
string “按原样读取”字符串,两端的空白字符(空格、换行符 \n、制表符 \t 等)会被忽略。空字符串变成 0。转换出错则输出 NaN

使用 typeof 来查询数据类型。注意的Feature:null->class function(本应为class)->function 交互方法:alert(msg), prompt(msg,[initial_input]), confirm(question) 只要有一个字符串,+变为concatenate,单元的+可以等同为转换为数字。优先级一元>二元(幂>乘除>加减) 字符串比较为Unicode字典序,数字比较显然,不同类型转化为Number再比较。严格相等/不等不转换类型。(严格运算为三个字符,不严格运算为两字符) 除了 nullundefined 两者之间:null==undefined, null!==undefined(官方CP,null 只与 undefined 互等);在与其他东西比较的时候都会转化为NumberNaN与别的数字比较总会是False! 布尔转换(除了"\t0\n"之类的都相当于->Number->Boolean):

Object

Basic

JS定义对象的语法非常反人类。(定义类的语法更是一坨,直到ES6才有) 类似Python,JS的key(必须是字符串,但是无空格的字符串使用的是自然写法)-value可以随时创建(对于实例直接赋值)与删除(delete Instance.attr),你甚至可以使用多个单词的key(要求使用""包括,引用时使用Object["String"]——这个用法对于单个单词的key也适用,下文中user["name"]也是合法的)。 对象的属性名甚至可以使用保留字,或者是数字等等(在找key的情况下,所有[]里面的东西都会被自动转化为字符串,但你依然可以使用纯数字+方括号来索引)。

let key = "double words"
let attr = prompt("Please input a string: ")
let user = {
  name: "John",
  age: 30, //建议最后的属性要带上逗号
  [attr]: undefined, //这种带中括号的即 计算属性,可以直接算出来中括号中的值作为key
}
user[key] = true;

定义的时候可以使用属性值简写,有点像构造函数:

function makeUser(name, age) {
  return {
    name, // 与 name: name 相同
    age,  // 与 age: age 相同
    // ...
  };
}

但是不一定要使用函数的形式,在普通的let语句中也可以这样用(别害怕,找不到的时候会给你undefined的)。判断是否有某个属性的方法为key in Object。 对于对象,可以使用for.in来遍历key:

for(let key in Obj) {
    alert(`key-value: ${key}-${Obj[key]}`);
}

排列顺序:整数属性会排序,剩下的按照定义顺序。

整数属性:先强制转换为Number再强制转换为String与原来相同的key("49"是,"+49","1.2"均不是)。

为了防止排序,你的key可以写成"+Integer"的形式。这样就会被认为这是一个字符串。

依然类似Python,任何mutable的东西(比如对象)都是引用赋值,共享地址。 因此,比较对象时,当且仅当地址相等,==, ===的结果才是True(类似is) 如果你需要拷贝对象:一、使用for.in挨个赋值;二、Object.assign(target, src1, src2, ...)。 如果你想要deepclone:你得调库,或者递推地拷贝。 所有不可达的对象都会被自动垃圾回收。 方法定义:(在类继承时会有区别)

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// 方法简写看起来更好,对吧?
let user = {
  sayHi() { // 与 "sayHi: function(){...}" 一样
    alert("Hello");
  }
};

可选链:value?.prop返回 value既不是null也不是undefinedvalue.propundefined。 而且还有 ?.(),?.[] 的变体。总结就是:只要左边是undefined就会直接短路。

典中典之this

JS中任何东西都会有this。对于类方法,this就是当前对象(当你要调用属性的时候必须使用this)。 所有的this都是在运行的时候即时算出来的。 没有this绑定。箭头函数没有this

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)

sayHi = function() {
  alert(this);
}

sayHi(); // undefined(严格模式,若非严格模式得到全局对象),且若使用 this.name 会报错

如果想要链式调用(调用方法后产生一个可以使用的对象),你可以参考以下例子:

let ladder = {
    step: 0,
    up() {
        return {
            let newObj;
            Object.assign(newObj, this);
            newObj.step++;
            return newObj; 
        }//这种是不希望原来的函数更改
        /* 如果希望更改;{this.step++; return this;} 可以省内存*/
    },
    showStep() {
        alert(`step=${this.step}`);
        return this;
    }
}

建议参考后文的习题来进一步学习。

幽默构造函数

构造函数并非定义在类中(或者是类方法)!所有构造函数都会是普通函数。

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack"); //new可能会迟到,但绝对不会缺席
                             //如果没有参数,可以省略括号(new User),但不建议这样写

alert(user.name); // Jack
alert(user.isAdmin); // false
//等价的写法: user = new function(){this.name = name;this.isAdmin = false;}

new.target

function User() {
  alert(new.target);
}

// 不带 "new":
User(); // undefined

// 带 "new":
new User(); // function User { ... }(返回该函数)
// 顺带一提,new Obj(...)[...]的语法是合法的

这样可以提供一种不需要new的神奇方法(库中常用,但是要慎用):

function User(name) {
  if (!new.target) { // 如果你没有通过 new 运行我
    return new User(name); // ……我会给你添加 new
  }

  this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John

一般来说,构造函数是不需要return。但是如果有return,当且仅当返回对象时返回此对象(并忽略this),否则返回原始类型时忽略return

Symbol

只有这个数据类型与String能够作为对象的键值。 并且它不会被自动转换成字符串,而是报错(只能.toString()或者.description)。 它跟 Ruby 的 Symbol 也是不一样的。尽管很像。

let id1 = Symbol("id");
let id2 = Symbol("id");//key可以留空(无参数)

alert(id1 == id2); // false,这是对象

看起来没什么用,但是如果你使用第三方的对象的时候,使用Symbol可以防止不同组建之间对对象的修改不会互相影响。也即,Symbol属性为“隐藏”属性。 如果在定义对象时使用Symbol,key值必须使用方括号。并且for.in不会遍历到Symbol。Object.keys(user) 也会忽略它们。但是Object.assign 会同时复制字符串和 symbol 属性。 如果你想要防止重复创建Symbol可以使用Symbol.for(key),在不存在此symbol时会创建一个,否则会返回已经创建的值。但是它依然不是Ruby。 Symbol.keyFor(sym)可以反向由符号查询值(仅限全局的Symbol)。上面代码框中的均不是全局量,直接查询会得到 undefined。因此此时只能用.toString()或者.description

注:有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 symbol。但大多数库、内建方法和语法结构都没有使用这些方法。

Primitive Value

JS中没有重载运算符。(这语言是怎么存活到现在的?) 三种类型转换(hint):String(各类字符串操作)、Number(各种数学操作,包括含大于小于的比较)、Default(二元加法、==等,通常除了Date对象都和"number"一致)。 为了进行转换,JavaScript 尝试查找并调用三个对象方法:

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试调用 obj.toString()(优先) 或 obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number""default" —— 尝试调用 obj.valueOf()(优先) 或 obj.toString(),无论哪个存在。
    obj[Symbol.toPrimitive] = function(hint) {
    // 这里是将此对象转换为原始值的代码
    // 它必须返回一个原始值
    // hint = "string"、"number" 或 "default" 中的一个
    }

    如果 toStringvalueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。 默认情况下,普通对象具有 toStringvalueOf 方法:

    • toString 方法返回一个字符串 "[object Object]"
    • valueOf 方法返回对象自身。

Attributes

对象属性除了value,还有三个标志:

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');//查成分

alert( JSON.stringify(descriptor, null, 2 ) ); / 属性描述符: { "value": "John", "writable": true, "enumerable": true, "configurable": true } /

修改属性标志:使用`Object.defineProperty(obj, propertyName, descriptor)`。最后一个参数为“属性描述符”对象(即上文代码块中`getOwnPropertyDescriptor`返回的格式)。如果属性描述符里面没有某种flag则默认为`false`。当属性不存在时会使用`descriptor`创建参数,否则会更新其值。
一般默认的内置类型转换等方法的`enumerable`为`false`(无法使用`for.in`或`Object.keys()`),但是如果显性重新定义后则`enumerable`变为`True`。
一次修改多个:`Object.defineProperties(obj, { prop1: descriptor1, prop2: descriptor2 , ...});`
一次获得所有属性描述符:`Object.getOwnPropertyDescriptor(obj)`。
### Accessor
有点Python的`@property`内味了。
```javascript
let obj = {
  get propName() {/* 当读取 obj.propName 时,getter 起作用*/},
  set propName(value) {/* 当执行 obj.propName = value 操作时,setter 起作用 */}
};

相应地,访问器属性描述符没有writable,取而代之的是无参数函数get()和单参数函数set(value)。一个属性也要么是数据类型(有value)要么是访问器类型(get/set)。

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

命名约定:以下划线开头的属性(例如_name)是内部属性,不应从外部访问,你可以使用get/set对其访问/赋值来操作。 代码更新时兼容旧属性的例子:(参见)

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // 年龄是根据当前日期和生日计算得出的
  Object.defineProperty(this, "age", { //构造函数内也可以直接这么写!
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday 是可访问的
alert( john.age );      // ……age 也是可访问的

Prototype Inheritance

你没有看错,在定义类之前我们就有原型和继承了。 原型使用隐藏属性[[Protytype]]实现。虽然你无法访问,但可以使用obj.__proto__来设置(它是这个隐藏属性的getter/setter)。在当前对象中找不到的属性会自动从原型中寻找。可以多层继承。但是原型只能有一个。 this属性永远是.前面的东西!!!继承仅仅影响方法,不会继承对象状态。 for.in会遍历继承的属性。obj.hasOwnProperty(key)返回是否是自己的属性(非继承)。笑点解析:这个函数本身就是继承的。 几乎其他所有的key/value获取方法都会忽略继承的属性(包括Object.keys/values/entries(obj)delete等)。当然你重新赋值了一遍就相当于不是继承的了。

注意:绑定的不是某个名字,而是当时与名字绑定的对象。

如果想要用构造函数来绑定原型,可以给F.prototype属性赋值(不知道函数属性可以看[[#函数对象]]),之后使用new操作构造的对象的.__proto__就会与F.prototype共享一个对象。 默认的函数原型为F.prototype = { constructor: F };。因此,如果你想要保留constructor属性,你可以重新赋值或者只给prototype赋属性值。

关于原型的进一步思考: 我们知道默认的object.toString()返回的值为"[object Object]",但是它来自哪里?答案是来自系统内置的构造函数Object(),并且Object.prototype存储着属性toString()。 同理,其他对象(ArrayDateFunction等等)都在prototype上面挂载了方法。而任意的arr/date/func.__proto__.__proto__ === Object.prototype。 问题来了,原始值(参见后文中[[#Primitive]])没有对象而只有对象**包装器**。也就是说访问原始值的属性/方法时,临时包装器会使用内置的构造器String, Number, Boolean等被创建。因此你可以通过String.prototype来给每个字符串来加个新的对象。(对其它类型同理,但是**最好不要这样做!**)你只能在 polyfilling 的时候这样做。不过还有另一种用法:方法借用。 当然,现在也不建议使用直接给proto`来设置和读取原型了(这一般只适用于浏览器,虽然对于服务器环境大多数也能用),建议使用:

Types

超级比较相等的方法:SameValueZero(x,y)参见ecma。

Primitive

原始类型也有方法(不包括nullundefined),但是为了尽可能保证原始的数据类型尽可能轻量,实现方法为“对象包装器”。注意对象包装器不能写入数据。

parseInt(str, [radix]), parseFloat(str, [radix])从第一个字符开始读取字符,直到无法被解析为数字为止,返回一个Number。可以读取带单位的值,例如"12px"

--- --- \uXXXX 以 UTF-16 编码的十六进制代码 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版权符号 © 的 Unicode。它必须正好是 4 个十六进制数字。 \u{X…XXXXXX}(1 到 6 个十六进制字符) 具有给定 UTF-32 编码的 Unicode 符号。一些罕见的字符用两个 Unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。
查询长度:str.length;索引直接使用方括号(没有负数索引,必须得要查询常数),返回长度为1的字符串,找不到返回undefined;为不可变类型,不能改变索引处的值。 常见属性:str.toUpperCase(), str.toLowerCase()str.indexOf/lastIndexOf(substr, [pos])pos开始查找,找不到返回-1,判断不是-1的技巧:按位非~;如果只需要查找是否存在可以str.includes/startsWith/endsWith(substr, [pos])(返回布尔值);str.split(char)/arr.join(char) 方法 选择方式…… 负值参数
slice(start[, end]) startend(不含 end 允许
substring(start[, end]) startend(不含 end,若前比后大则交换) 负值被视为 0
substr(start[, length]) start 开始获取长为 length 的字符串 允许 start 为负数

str.codePointAt(pos)String.fromCodePoint(code)实现UTF码与字符的互相转换。 有时候为了恰当地比较一些有帽子的字符,可以使用str.localeCompare(str2),str靠前返回-1,靠后返回+1,相同返回0。(还可以加额外的参数来指定比较形式,参见docs);str.trim()可以删除字符串前后的空格。

Iterable

比方说Python中的range就是可迭代的(以及yield等等)。只有可迭代对象才能用for.of

let range = {
  from: 1,
  to: 5
};

// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {

  // ……它返回迭代器对象(iterator object):
  // 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
  return {
    current: this.from,
    last: this.to,

    // 3. next() 在 for..of 的每一轮循环迭代中被调用
    next() {
      // 4. 它将会返回 {done:.., value :...} 格式的对象
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// 现在它可以运行了!
for (let num of range) {
  alert(num); // 1, 然后是 2, 3, 4, 5
}

for.of等价于以下显式调用迭代器的过程:

let str = "Hello";

// 和 for..of 做相同的事
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 一个接一个地输出字符
}

// 将 str 拆分为字符数组 let chars = Array.from(str);

alert(chars[0]); // 𝒳 alert(chars[1]); // 😂 alert(chars.length); // 2

-------------------
JS中还有 `Map`和`Set`(我嘞个STL啊)。两者都可以使用`for.of`迭代,顺序与插入顺序相同 。
`Map`可以使用任何类型的key。**不要使用方括号索引**,不然这个美好的特性就用不上了。你可以使用对象作为key(如果使用普通的方括号索引,两个字符串都会被`.toString()`变成`"[object Object]"`)。
- `new Map()` —— 创建 map,参数可以替换为一个两列的数组或其他可迭代对象。
- `map.set(key, value)` —— 根据键存储值,返回`map`本身,因此可以链式调用(叠加多个`.set(a, b)`)。
- `map.get(key)` —— 根据键来返回值,如果 `map` 中不存在对应的 `key`,则返回 `undefined`。
- `map.has(key)` —— 如果 `key` 存在则返回 `true`,否则返回 `false`。
- `map.delete(key)` —— 删除指定键的值。
- `map.clear()` —— 清空 map。
- `map.size` —— 返回当前元素个数。
- `map.keys()` —— 遍历并返回一个包含所有键的可迭代对象。迭代的顺序与插入值的顺序相同。
- `map.values()` —— 遍历并返回一个包含所有值的可迭代对象,
- `map.entries()` —— 遍历并返回一个包含所有实体 `[key, value]` 的可迭代对象,`for..of` 在默认情况下使用的就是这个。
- `map.forEach(function(value, key, map){...})`与`Array`的`forEach`相似。
如果我们想从一个已有的普通对象(plain object)来创建一个 `Map`,那么我们可以使用内建方法 [Object.entries(obj)](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/entries),该方法返回对象的键/值对数组,该数组格式完全按照 `Map` 所需的格式。`Object.fromEntries` 方法的作用是相反的:给定一个具有 `[key, value]` 键值对的数组,它会根据给定数组创建一个对象。
`Set`相当于只有value没有key。
- `new Set(iterable)` —— 创建一个 `set`,如果提供了一个 `iterable` 对象(通常是数组),将会从数组里面复制值到 `set` 中。
- `set.add(value)` —— 添加一个值,返回 set 本身。重复添加同一个值不会改变`set`。
- `set.delete(value)` —— 删除值,如果 `value` 在这个方法调用的时候存在则返回 `true` ,否则返回 `false`。
- `set.has(value)` —— 如果 `value` 在 set 中,返回 `true`,否则返回 `false`。
- `set.clear()` —— 清空 set。
- `set.size` —— 返回元素个数。
- `set.keys()` —— 遍历并返回一个包含所有值的可迭代对象,
- `set.values()` —— 与 `set.keys()` 作用相同,这是为了兼容 `Map`,
- `set.entries()` —— 遍历并返回一个包含所有的实体 `[value, value]` 的可迭代对象,它的存在也是为了兼容 `Map`。
- `set.forEach(function(value, valueAgain, map){...})`——注意参数的变化。
还有一种查询key-value的方法:`Object.keys/values/entries(obj)`。三者均应该返回**数组**而不是上文中类似的可迭代对象(历史遗留问题)。
> `for.in`循环会忽略`Symbol`属性,`Object.keys/values/entries(obj)`也是。
> 如果你想要 `Symbol`类型的key,可以使用[Object.getOwnPropertySymbols](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols)(仅包含Symbol的键)或者[Reflect.ownKeys(obj)](https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys)(返回 **所有** 键)。

------------
JS中更加神人的`WeakMap`和`WeakSet`:
`WeakMap`的key必须是对象而不能是原始值。并且,其中的作为Key的对象必须存在其他引用,否则会被垃圾回收!幽默弱引用。
```javascript
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用

// john 被从内存中删除了!

WeakMap 不支持迭代以及 keys()values()entries() 方法(因为内存回收是引擎决定何时执行的,因此这些方法的结果无法确定)。只有以下的方法:

Generator

喜欢yield吗?那么你就要使用function*

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" 创建了一个 "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]

赋值的时候,迭代器里的代码还没有真正运行。 当然每个迭代器都是可以调用.next()的,与前文相仿,里面会有valuedone两个属性。 使用for.of循环时不会遍历最后的return值,你可以把它改成yield。 我们可以把上文中某个例子改成更加紧凑的形式:

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1,2,3,4,5

套娃:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
  yield* generateSequence(48, 57);// 0..9
  yield* generateSequence(65, 90);// A..Z
  yield* generateSequence(97, 122);// a..z
}

generator不是一个纯输出的结构,你当然可以给它整上输入(generator.next(input))。

function* gen() {
  // 向外部代码传递一个问题并等待答案
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 将结果传递到 generator 中

你甚至还能给generator报错(generator.throw(err)):

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // 显示这个 error
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

如果generator没有接住这个error,它会从函数中掉出来,你可以在generator.throw外面在套上一层try.catch来解决。 generator.return(value)可以使当前迭代器变成{value, done: true}。 与async一起的用法参见后文。

Assignment

Python:这还用学?

let [firstName, surname] = arr;
//let firstName = arr[0], surname = arr[1];
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]); //这两者都是迭代器发力了
let user = {
  name: "John",
  age: 30
};
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest 是包含从第三项开始的其余数组项的数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
// 只会提示输入姓氏,能够被赋值的对象的缺省值会被忽略
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];

alert(name);    // Julius(来自数组)
alert(surname); // 你输入的值

let {height, width, title} = { title: "Menu", height: 200, width: 100 }
alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
let options = {
  title: "Menu",
  width: 100,
  height: 200
};
// { sourceProperty: targetVariable }
let {width: w /*=prompt("width? ") 在能够赋值时被忽略*/, height: h, title} = options; //这个语法可能和直觉相反

let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}

注意:在不使用let时,引擎可能会把大括号认为是代码块;为了避免这种情况,要在语句的两端加上()。 以上这些代码可以嵌套(例子略)。

智能传参:感觉不如Python。(注意,调用函数的时候参数不能留空,而数组可以。)

let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

function showMenu({
  title = "Untitled",
  width: w = 100,  // width goes to w
  height: h = 200, // height goes to h
  items: [item1, item2] // items first element goes to item1, second to item2
} = {}) { //You can use showMenu() instead of showMenu({})
  alert( `${title} ${w} ${h}` ); // My Menu 100 200
  alert( item1 ); // Item1
  alert( item2 ); // Item2
}

showMenu(options);

JSON

对象转换为JSON:JSON.stringify(object);JSON转换为对象:JSON.parse(json)。 注意:JSON中所有Key被改写为有双引号的字符串。 JSON支持的类型:Object Array String Number Boolean null JSON不包含:函数/方法、Symbol/undefined的key/value。不能存在循环引用。 完整版:JSON.stringify(value[, replacer, space])replacer代表要编码的属性数组或者映射函数,space表示格式化所需要的空格数量。

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup 引用了 room
};

room.occupiedBy = meetup; // room 引用了 meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
occupiedBy: [object Object]
*/

可以给对象自定义toJSON()方法。 JSON.parse(str, [reviver])的可选参数对每个(key, value)调用并转换,返回value注意:以上所用方法对于嵌套的元素均会起效果!

Function

可变参数

JS中原则上你可以传入任意个数的参数,当形参对于实参时多出部分变成undefined,反之则会忽略多余的实参。 如果想要收集多余参数,可以使用...args(可以使用其他名称)。一定要把它放在最后一位。args会成为一个数组。 有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。箭头函数没有arguments,与this类似。(它访问的arguments相当于外部函数的值。) 如果想要将数组/可迭代对象(不能直接用于类数组,必须得要Array.from())进行解包操作,依然可以在参数中写成...arr(此时当然不一定放在最后)。可以发现如果将...替换为*,以上的两个操作将会变为我们在Python中熟悉的形式。 因此,我们又获得了浅拷贝的另一种形式[...arr],{...obj}。 一个高级的柯里化实现:

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

作用域

letconst均为块作用域,无声明提升。JS中存在闭包。每次调用函数都会产生一个全新的词法环境。这块跟Python几乎一模一样(把所谓Lexical enviroment替换为Frame就一样了)。参见这一章的内容(强烈建议把习题都做一遍)。 JS中的全局对象为globalThis。在浏览器中,它叫做window;在Node.js中它叫做global,在别的环境中可能会有别名。 全局对象的属性/方法可以直接调用,省略globalThis.;使用var声明的全局变量会变为全局对象的属性(不要这么做!)。全局函数声明也会有同样的效果(函数表达式、箭头函数都不是函数声明)。你也可以直接编辑全局变量的某些属性,来等效地让它变为全局的。

函数对象

函数的名字可以使用func.name(没有括号!)来访问,即使在创建时使用函数表达式赋值依然能够正确地识别(上下文命名)。如果上下文命名失败,name=""func.length返回形参个数(不包括可变参数)。 在函数内部也可以定义属性(请与变量区分)。你可以用它来部分地代替闭包(如果你想要某个参数能被外部代码修改那就使用函数属性,否则使用闭包)。

function sayHi() {
  alert("Hi");
  // 计算调用次数
  sayHi.counter++;
}
sayHi.counter = 0; // 初始值
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times

对于命名的函数表达式(尽管有名字但依旧不是函数声明)有两个特性:在函数体内能够递归调用;在函数外其名称不可见。 正如“函数对象”其名,函数也可以使用new来构造:

let func = new Function ([arg1, arg2, ...argN], functionBody);

其中的所有参数均为字符串。

let sum = new Function('a', 'b', 'return a + b');
//let sum = new Function('a , b', 'return a+b');
alert( sum(1, 2) ); // 3

这种情况常见于要从服务器接受代码来动态编译函数。注意此时得到的函数的词法环境为全局变量!

装饰器种种

并没有美丽的@语法,你只能直接调用来函数装饰。当装饰到含有this的函数时,可以使用func.call(context, ...args)方法,第一个参数是为this

func(1, 2, 3); //this == globalThis
func.call(obj, 1, 2, 3); //this == obj

/*
function hash() {
    return [].join.call(arguments); //方法借用
}
*/

如果你不想传入参数列表而是一个类数组,那么将call变为apply即可。 如果你只想要得到一个绑定了this的函数,请使用func.bind(context)(一个函数只能被绑定一次)。bind结束后得到的是另一个对象,它不会保留原来加上的函数属性。

let bound = func.bind(context, [arg1], [arg2], ...);

是的,完整版的func.bind还可以绑定参数。这样比使用闭包来调用方便多了。但是如果你只想要绑定参数,你确实可以自定义一个东西:

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

=>

箭头函数没有this,没有arguments,它全部从外界获取。箭头函数不会离开当前的上下文。 同理,箭头函数不能作为构造器,不能将其用到new后面。它也没有super

Class

基础

笑点解析:继承都讲完了才出现类。

class MyClass {
  // class 方法
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}
let class = new MyClass();
alert(typeof User); // function

类方法之间没有括号! 正如上文,类是一种函数(令人忍俊不禁)。也就是说,MyClass === MyClass.prototype.constructor;上面的constructor与每个methodMyClass.prototype的方法。(也就是说,每个创建的对象相当于继承了类。) 那么,为什么我不直接使用普通的构造函数呢?通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true。因此,它与手动创建并不完全相同。例如,与普通函数不同,必须使用 new 来调用它;类有自己的toString类方法不可枚举(不能使用for.in);等等。 当然类也有类表达式之说,类表达式也可以有自己的姓名。

// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // error,MyClass 在外部不可见

同理,类也可以使用getter.setter等等,或者使用计算属性(中括号内部的东西)。 所谓类字段,就是在定义类的时候加入属性(这个在C/C++看起来这么显而易见的东西居然在最近才放入标准,可能需要polyfill)。

class User {
  name = "John"; //孩子们很多旧版本都不支持我
  //你甚至可以使用 name = prompt("Name, please?", "John") 这种语句。

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

所有类字段会挂在实例对象而非原型上。 回忆:在某些时候使用类方法做其它函数的参数会出现this丢失的问题:

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

这时候有三种方法:

  1. 传递一个包装函数,例如 setTimeout(() => button.click(), 1000)
  2. 将方法绑定到对象(func.bind),例如在 constructor 中。
  3. 在类中写下click = () => { alert(this.value) }。这对于事件监听非常有用。

继承

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }
}

let animal = new Animal("My animal");
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

此时 Rabbit.prototype.[[prototype]] === Animal.prototype。 注意,extends之后可以写任意表达式:

function f(phrase) {
  return class {
    sayHi() { alert(phrase); }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

找爹:使用super关键字来更加灵活地重写部分属性。(箭头函数依然没有super,只会从外部找。)

}

class Rabbit extends Animal { hide() { alert(${this.name} hides!); }

stop() { super.stop(); // 调用父类的 stop this.hide(); // 然后 hide } }

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stands still. White Rabbit hides!

哦天呐,我们还没有重写构造器呢!默认情况下如果没写构造器那就和`super`产生一样的行为。让我们来试试:
```javascript
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 不工作!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

为什么报错:一句话:继承类的constructor必须调用super(...),并且一定要在使用this之前调用! 为什么会这样?因为继承类的构造函数会存在特殊的隐藏标签[[Constructorkind]]:"derived"。因此它在new的时候行为会发生改变:

关于super的一切

你可能觉得调用super的方法只需要将其替换为this.__proto__就可以了:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style and call parent (animal) method
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears and call parent (rabbit) method
    this.__proto__.eat.call(this); // (**)
  }
};

longEar.eat(); // Error: Maximum call stack size exceeded

Oops,死循环。为了解决这种野蛮的错误,JS添加了特殊内部属性[[HomeObject]]。一个类/对象的方法的[[HomeObject]]就是该类/对象。于是调用super的时候就会从它的[[HomeObject]]中寻找。 很遗憾,[[HomeObject]]是永久绑定的,无法被更改。

let animal = {
  sayHi() {
    alert(`I'm an animal`);
  }
};

// rabbit 继承自 animal
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  }
};

// tree 继承自 plant
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();  // I'm an animal (?!?)

这时候方法借用时,super无法动态绑定。这使我们更加清晰地看出: 方法,不是函数属性!!!(方法有[[HomeObject]]而函数没有。) 所以,不要借用有super的函数。

static

想你了C++。static标签中调用的this就是类构造器。 static方法都是挂载在Class而非Class.Prototype。 通常,静态方法用于实现属于整个类,但不属于该类任何特定对象的函数。

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static compare(articleA, articleB) {
    return articleA.date - articleB.date;
  }
}

// 用法
let articles = [
  new Article("HTML", new Date(2019, 1, 1)),
  new Article("CSS", new Date(2019, 0, 1)),
  new Article("JavaScript", new Date(2019, 11, 1))
];

articles.sort(Article.compare);

alert( articles[0].title ); // CSS

另一个例子:

class Article {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createTodays() {
    // 记住 this = Article
    return new this("Today's digest", new Date());
  }
}

let article = Article.createTodays();

alert( article.title ); // Today's digest

注意:静态方法不适用于单个对象。 当然你可以使用静态的类属性(很遗憾这依然是一个最近新增的feature)。静态属性可以继承(当然是在类与类之间而不是方法与方法之间)。

public, private, protected

没有这三个东西 没有这三个保留字,但是你可以某种程度上地实现他们。 public:不需要实现。 protected:没有语法层面的强制实现。我们一般约定单下划线的开头的类/对象属性/方法只能内部访问,读写操作使用对象方法或get/set实现。很显然这是可以继承的。 private:最近新添的feature允许私有属性与方法以#开头。调用的时候依然需要加上#,因此它们不会与其它属性字段发生冲突。它们不能使用obj.#aobj["#a"]等方式来访问。大部分情况下你只能使用polyfills实现。

内建的许多类(Array, Map等)都是可以扩展的。

// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false
alert(arr.constructor === PowerArray); // true

我们甚至可以给这个类添加一个特殊的静态 getter Symbol.species,它会返回 JavaScript 在内部用来在 mapfilter 等方法中创建新实体的 constructor。 如果我们希望像 mapfilter 这样的内建方法返回常规数组,我们可以在 Symbol.species 中返回 Array,就像这样:

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 内建方法将使用这个作为 constructor
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter(item => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

注:内建类不会继承静态方法。因为他们之间不是extends的关系;作为构造函数,它们之间只有.prototype存在继承关系([[Prototype]]);而它们本身之间不存在[[Prototype]]的关系。

类型检查

省流:

用于 返回值
typeof 原始数据类型 string
{}.toString 原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象 string
instanceof 对象 true/false
obj instanceof Class // 返回obj是否为Class或者其衍生类

执行逻辑:

  1. 如果这儿有静态方法 Symbol.hasInstance,那就直接调用这个方法:
  2. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准的逻辑是:使用 obj instanceOf Class 检查 Class.prototype 是否等于 obj 的原型链(不停叠加.__proto__)中的原型之一。

    Mixin

    JS没有多继承,所以就有了mixin,它是一个无需继承方法就能被其他类使用的类。

    
    // mixin
    let sayHiMixin = {
    sayHi() {
    alert(`Hello ${this.name}`);
    },
    sayBye() {
    alert(`Bye ${this.name}`);
    }
    };

// 用法: class User { constructor(name) { this.name = name; } }

// 拷贝方法 Object.assign(User.prototype, sayHiMixin);

// 现在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude!


## Error
经典try catch throw:
```javascript
try {
//...
} catch (err) {
//...
} // finally {} optional.无论如何都会执行

error的属性:namemessagestack(当前调用栈),等等。 抛出错误使用throw,后面可以接任何东西,但是最好使用含有namemessage属性的对象。 内建error对象:ErrorSyntaxErrorReferenceErrorTypeError 等。它们的构造器均接受(msg)的参量。 可以在catch块中加入各种instanceof来判断错误类型,甚至可以再次throw错误。 注意:即使try中有returnfinally中的内容依然会执行。

Async

回调

JS中的代码异步调用,所以在执行下文的操作时上文可能还并没有执行完成。 有一个很简单的解决方案(这里面用了一些DOM方法):

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script); //callback(err, script)传入你想要在加载之后执行的函数
  script.onerror = () => callback(new Error(`Script load error for ${src}`));
  document.head.append(script);
}

但是如果要多层嵌套的话就炸了,怎么办?答案是使用Promise对象。

let promise = new Promise(function(resolve, reject) {
  // 当 promise 被构造完成时,自动执行此函数

  // 1 秒后发出工作已经被完成的信号,并带有结果 "done"
  setTimeout(() => resolve("done"), 1000);
});

当 executor 获得了结果(以下两者只能最终出现一个,之后的都会被忽略):

// .catch(f) 与 promise.then(null, f) 一样 promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

还有`promise.finally(f)`的方法,它类似于`.then(f, f)`,区别:
- `finally` 处理程序(handler)没有参数。在 `finally` 中,我们不知道 promise 是否成功。这没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。
- `finally` 处理程序将结果或 error “传递”给下一个合适的处理程序。
例如,在这结果被从 `finally` 传递给了 `then`:
```javascript
new Promise((resolve, reject) => {
  setTimeout(() => resolve("value"), 2000)
})
  .finally(() => alert("Promise ready")) // 先触发
  .then(result => alert(result)); // <-- .then 显示 "value"

总结:

}).then(function(result) {

alert(result); // 1

return new Promise((resolve, reject) => { // () setTimeout(() => resolve(result 2), 1000); });

}).then(function(result) { // (**)

alert(result); // 2

return new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); });

}).then(function(result) {

alert(result); // 4

});


**如果 `.then`(或 `catch/finally` 都可以)处理程序返回一个 promise,那么链的其余部分将会等待,直到它状态变为 settled。当它被 settled 后,其 result(或 error)将被进一步传递下去。**