Skip to content

JavaScript 语言(负责前端行为)

· 解释性语言:JavaScript、php

优点:跨平台

逐行翻译成机械语言

缺点:稍微慢

· 编译性语言:c、c++

优点:快

通篇翻译成机械语言

缺点:移植性不好(不跨平台)

· oak 语言:Java 的前身(现在不是了)

1、JavaScript 是单线程

2、ECMA 标注:为了取得技术优势,微软推出了 JScript,CEnvi 推出 ScriptEase,与 JavaScript 同样可在浏览器上运行。为了统一规格 JavaScript 兼容于 ECMA 标准,因此也称为 ECMAScript。

现如今有:ES3.0、ES5.0、ES6.0......

3、轮转时间片:两个任务都需要执行时

image-20210114152509318

4、js 三大部分 ECMScript、DOM、BOM:DOM 是对 html 进行操作、BOM 是对浏览器进行操作(每个浏览器差别很大)

5、引入方式:<script type="text/javascript"></script><script type="text/javascript" src=""></script>

引入外部 js 的理由:可维护性、缓存、适应未来

6、js 运行三部曲:语法分析、预编译、解释执行

声明变量

ECMAScript 变量是松散类型的,变量可以保存任何类型的数据,每个变量只不过是一个用于保存任意值的命名占位符。

声明了但是未初始化的变量将会自动赋予 undefined 值;未声明变量会报错,但是 typeof 会返回 undefined,而且 delete 操作也不会报错。

1.var

所有 ECMAScript 版本均可用

var 是函数作用域

var 定义的变量可以修改,如果不初始化会输出 undefined,不会报错

var 声明的变量在其作用域中会进行代码提升,重复的声明将会在顶部合并为一个声明

var 在全局作用域中声明的变量会成为 window 对象的属性

当不使用 var 进行定义时,变量默认的 configurable 为 true,可以进行 delete 等命令进行操作,而当 var 在定义一个全局变量的时候 configurable 变为了 false,即不会被 delete 删除

**暗示全局变量(imply global):**如果一个变量未经声明就被初始化,此变量将为全局对象(window)所有,被添加到全局上下文

a = 10; --> window.a = 10;

全局上的所有变量也归全局对象(window)所有,所以说,window 就是全局的域

javascript
var b = 10; --> window.b = 10;

a = 10; --> window.a = 10;
console.log(a); --> console.log(window.a);

function test(){
    var a = b = 123; --> b = 123; var a = b;
}
test();
//控制台打印:
window.a:undefined
window.b:123

2.let

只能在 ECMAScript6 以及更新版本中使用

let 是块级作用域,函数内部使用 let 定义后,对函数外部无影响

let 拥有块级作用域,一个 {代码} 就是一个块级作用域,也就是 let 声明块级变量,即局部变量(只要是{}中定义的 let,就不能被{}外访问)

let 在其作用域中不能被重复声明(函数作用域和块级作用域)

严格来说,let 在 JavaScript 运行时也会被提升,但由于”暂时性死区“,let 不能在声明前使用变量

在 let 声明之前执行的瞬间被称为”暂时性死区“(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出:ReferenceError

let 在全局作用域中声明的变量不会成为 window 的对象属性

3.const

只能在 ECMAScript6 以及更新版本中使用

const 定义的变量不可以修改,而且必须初始化,但是,当 const 对象是引用值的时候,可以修改引用值内部属性,但是不能修改对象的引用,例如:

javascript
const person = {};
person.name = "lsn";
// correct

const name = 1;
name = 2;
// wrong

const 的作用域与 let 相同,只在声明所在的块级作用域内有效,并且也是和 let 一样不可以重复进行声明,不能在定义前访问变量

const 在全局作用域中声明的变量不会成为 window 的对象属性

多个赋值规范:

javascript
var a,
	b,
	c=100;
1.变量名必须以英文字母、_、$开头
2.变量名可以包括英文字母、_、$、数字
3.不可以用系统的关键字、保留字作为变量名

值的分类

原始值:number boolean string undefined null symbol (es6 新增,表示符号)

var a = -123.234; var a = "hello world"; var a = true/false; var a = undefined; var a = null;

”原始值不能添加动态属性“,即"abc".aa = '7';这样不会报错,但是立马就不能访问到该属性了,造成此影响的原因是包装类,后文有解释

原始值为栈内存中存储

number:

可能存在+0-0

八进制:必须以0开头,如var num = 075;(在严格模式下无效,会抛出语法错误),es6 中使用的是0o

十六进制:必须以0x开头,如var num = 0x1f;

无论是八进制还是十六进制,在所有数学操作中都被视为十进制数值;

值的范围:Number.MIN_VALUE = 5e-324; -> Number.MAX_VALUE = 1.7976931348623157e+308; (大部分浏览器中)

如果值超出了这个范围,这个值将会被转换为Infinity-Infinity,可以使用isFinite()函数确定一个数是不是有限大

isFinite(Number.MAX_VALUE + Number.MAX_VALUE); => false

Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY可以获取到-InfinityInfinity

NaN(not a number):表示本来要返回的数值操作失败了,比如 0、+0、-0 相除会返回 NaN,而非零值除以 0 或-0 会返回 Infinity 或-Infinity,任何设计 NaN 的操作都会返回 NaN,NaN 不等于包括 NaN 在内的任何值

string:

用``、"、'囊括的字符串,相关字符字面量:转义字符`、换行\n、制表\t、退格\b、回车\r、换页\f、十六进制 nn 表示的字符\xnn、十六进制 nnnn 表示的 Unicode 字符\unnnn,这些字面量虽然实际长度不为 1,但是在 string 中这些转义序列只表示一个字符

字符串是不可变的,一旦创建其值就不能改变了,例如:

javascript
var lang = "abc";
lang = lang + "bb";
//首先会分配一个可以容纳5个字符的空间,然后将abc和bb填入,最后销毁“abc”和“bb”

toString():几乎所有对象都有toString()方法,除了 null 和 undefined

模板字面量:用`囊括的字符串,模板字面量会保留反引号中所有的空格回车,模板字面量不是字符串,而是一种特殊的 JavaScript 语法表达式,只不过求值后得到的是字符串,模板字面量可以插值,例如:

javascript
// before
let str = value + " hello " + value2 * 2;
// now
let str = `${value} hello ${value2 * 2}`;
//所有插入的值都会用toString()强转为字符串型,而且任何JavaScript表达式都可以用于插值

模板字面量标签函数:直接看例子理解通透一点

javascript
let a = 6,
  b = 9;

function test(strings, aval, bval, sumval) {
  console.log(strings); //第一个参数是以插值为分割符,将字符串分割出来的一个数组
  console.log(aval);
  console.log(bval);
  console.log(sumval);
  return "foobar";
}

let testStr = `${a} + ${b} = ${a + b}`;
let testRes = test`${a} + ${b} = ${a + b}`;

console.log(testStr);
console.log(testRes);

image-20210705133741015

因为表达式的参数是可变的,通常我们都会使用剩余操作符将他们收集到一个数组中,如:

javascript
function test(strings, ...expressions) {
  for (const expression of expressions) {
    conosle.log(expression);
  }
}
// 6 5 11

如果要获取原字符串:

javascript
function test(strings, ...expressions) {
  return strings[0] + expressions.map((val, index) => `${val}${strings[i + index]}`).join("");
}

原始字符串

javascript
console.log(`\u00A9`); //©
console.log(String.raw`\u00A9`); //\u00A9

console.log(`asdf \n aaa`); //会换行
console.log(String.raw`asdf \n aaa`); //不会换行

console.log(`afda
aaa`); //会换行
conaole.log(String.raw`afda
aaa`); //会换行

function test(strings) {
  for (const string of strings) {
    console.log(string); //会返回转义字符
  }

  for (const string of strings.raw) {
    console.log(string); //会返回实际字符串
  }
}
symbol:

es6 新增数据类型,它是原始值,且是唯一的、不可变的,用途是确保对象属性使用唯一标识符,不会发生属性冲突危险,它是用来创建唯一记号,进而作用非字符串形式的对象属性。**Symbol()**不能做构造函数,所以不能和 new 配合使用,入果想使用符号包装对象,可以借助 Object 的力量,像这样let mySymbolObj = new Object(Symbol());

Symbol()

创建一个符号,let sym = Symbol();,同时可以传入一个字符串用作对该符号的描述,将来可以通过这个字符串调试代码,但是这个字符串参数与符号定义或标识完全无关。

javascript
let a = Symbol();
let b = Symbol();
console.log(a == b); //false

let c = Symbol("foo");
let d = Symbol("foo");
console.log(c == d); //false

console.log(c); //Symbol(foo);

利用 symbol 的唯一性可以把它当作对象属性,因为 symbol 的普通定义是不会有重复的。

**全局符号注册表:**如果不同部分需要用同一个符号实例,可以用一个字符串作为键在全局符号注册表中创建并使用符号:Symbol.for('foo'),每次调用此方法都会在整个全局运行时注册表中检查,如果有相同键值的则返回同一个实例,如果没有则创建新的实例并返回。不过全局注册表中定义的符号还是不等于Symbol()定义的符号。

全局注册表中的符号必须用字符串键来创建

javascript
let a = Symbol.for("foo");
let b = Symbol.for("foo");

a === b; //true

let c = Symbol("foo");
c === a; //false

作为参数传给Symbol.for()的任何值都会被转换为字符串(调用 toString 方法),同时也会作为该符号的描述

javascript
let b = Symbol.for("foo");
let c = Symbol.for(new Object());
console.log(b, c);
console.log(new Object().toString());

image-20210706094918995

Symbol.keyFor()

用来查询全局注册表,传入参数为符号,返回符号对应的字符串键,如果查询的不是全局符号则返回 undefined,如果查询的不是符号则返回 TypeError 报错

Symbol.keyFor(Symbol.for('foo')); //foo

**使用符号作为属性:**凡是可以使用字符串或数值作为属性的地方都能够使用符号,包括了对象字面量属性和Object.defineProperty()/Object.definedProperties()定义的属性,对象字面量只能在计算属性语法中使用符号作为属性。

javascript
let a = Symbol("foo");
let b = Symbol("boo");
let c = Symbol("zoo");
let d = Symbol("aoo");

let o = {
  [a]: "one",
};
//或者
o[a] = "one";

//	o = {Symbol(foo): one}

Object.defineProperty(o, b, { value: "two" });

//	o = {Symbol(foo): one, Symbol(boo): two}

Object.definedProperties(o, {
  [c]: { value: "three" },
  [d]: { value: "four" },
});

//	o = {Symbol(foo): one, Symbol(boo): two, Symbol(zoo): three, Symbol(aoo): four}

Object.getOwnPropertyNames()返回对象实例常规性属性数组(返回常规键数组)

["a", "b"]

Object.getOwnPropertySymbols()返回实例对象的符号属性数组,与第一个方法返回值互斥(返回 Symbol 键数组)

[Symbol(foo), Symbol(bar)]

Object.getOwnPropertyDescriptors()会返回包有常规属性和符号属性描述符的对象(返回键值对对象)

image-20210706104338910

Reflect.ownKeys()会返回两种类型的键(键数组)

["a", "b", Symbol(foo), Symbol(bar)]

因为符号是对内存中符号的一个引用,所以直接创建并作用属性的符号并不会丢失,如果没有显示的保存对这些属性的引用,就必须遍历对象所有符号属性才能找到相应的属性键。

javascript
let o = {
  [Symbol("foo")]: "foo val",
  [Symbol("bar")]: "bar val",
};

let barSymbol = Object.getOwnPropertySymbols(o).find((symbol) => symbol.toString().match(/bar/));

Object.getOwnPropertySymbols(o).find((symbol) => symbol.toString());
//Symbol(foo)
//Symbol(bar)
常用内置符号:

es6 引用了一批常用内置符号,用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为,这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。(符号在 ECMAScript 规范中表示@@,@@iterator = Symbol.iterator)

所有的内置符号属性都是不可写、不可枚举、不可配置的

Symbol.asyncIterator(暂时搁置)

该属性表示:一个方法,返回该对象默认的 AsyncIterator,由 for-await-of 语句使用。

也就是说这个符号表示实现异步迭代器 API 的函数。循环时,会调用以 Symbol.asyncIterator 为键的函数,并返回一个实现迭代器的 API 对象,很多时候返回的是实现该 API 的 AsyncGenerator

Symbol.iterator

该属性表示:一个方法,该方法返回对象的默认迭代器,由 for-of 语句使用

循环时,会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象,很多时候返回的对象时实现该 API 的 Generator

javascript
class Foo {
  *[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]()); //Generator {<suspended>}

//技术上,这个由Symbol.iterator生成的对象应该通过其next()方法陆续返回值,可以显式地调用next()方法返回,也可以隐式地通过生成器函数返回
class Emitter {
  constructor(max) {
    this.max = max;
    this.idx = 0;
  }

  *[Symbol.iterator]() {
    while (this.idx < this.max) {
      yield this.idx++;
    }
  }
}

function count() {
  let emitter = new Emitter(5);

  for (const x of emitter) {
    console.log(x);
  }
}
count(); //0 1 2 3 4
Symbol.hasInstance

该属性表示:一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例,由 instanceof 操作符使用

javascript
function Foo() {};
let f = new Foo();
f instanceof Foo;	//true

class Bar();
let b = new Bar();
b instanceof Bar;	//true

//实现原理

function Foo() {};
let f = new Foo();
Foo[Symbol.hasInstance] (f);	//true

...
Bar[Symbol.hasinstance] (b);	//true

这个属性定义在 Function 的原型上,默认在所有的函数和类上都能调用。

因为 instanceof 会在原型链上寻找这个属性的定义,和在原型链上寻找其他属性一样,所以可以在继承的类上通过静态方法重新定义这个函数

javascript
class Bar();
class Baz extends Bar {
    static [Symbol.hasInstance] () {
        return false;
    }
}

let b = new Baz();
Bar[Symbol.hasInstance] (b);	//true
b instanceof Bar;	//true
Baz[Symbol.hasInstance] (b);	//false
b instanceof Baz;	//false
Symbol.isConcatSpreadable

该属性表示:一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素

es6 中的 Array.prototype.concat()方法会根据就接收到的对象类型选择如何将一个类数组对象拼接成数组实例,覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为

该值为 true 或真值的时候会将类数组对象打平到末尾,非类数组对象忽略;其值为 false 或假值的时候将会将整个对象直接追加到整个数组末尾

javascript
let ini = ["foo"];
let arr = ["bar"];
console.log(arr[Symbol.isConcatSpreadable]); //undefined
ini.concat(arr); //['foo', 'bar']
arr[Symbol.isConcatSpreadable] = false;
ini.concat(arr); //['foo', arr(1)]

let arr = { length: 1, 0: "baz" };
ini.concat(arr); //['foo', {...}]
arr[Symbol.isConcatSpreadable] = true;
ini.concat(arr); //['foo', 'baz']

let obj = new Set().add("qux");
ini.concat(obj); //['foo', Set(1)]
arr[Symbol.isConcatSpreadable] = true;
ini.concat(obj); //['foo']
Symbol.match

该属性表示:一个正则表达式方法,该方法用正则表达式匹配字符串,由 String.prototype.match()方法使用

String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

RegExp.prototype[Symbol.match]; // f [Symbol.match] () { [native code] }

'foo'.match(/bar/);

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。可以重新定义 Symbol.match 函数从而让 match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例,返回值没有限制。

javascript
class Foomatcher {
  static [Symbol.match](target) {
    return target.includes("foo");
  }
}

"foobar".match(Foomatcher); //true
"bar".macth(Foomatcher); //false

class StringMatcher {
  constructor(str) {
    this.str = str;
  }

  [Symbol.match](target) {
    return target.includes(this.str);
  }
}

"foobar".match(new StringMatcher("foo")); //true
"bar".match(new StringMatcher("foo")); //false
Symbol.replace

该符号特性与 Symbol.match 一样,只不过 Symbol.replace 函数能接收两个参数,分别为调用 replace 方法的字符串实例和替换的字符串

javascript
class FooReplace {
  static [Symbol.replace](target, replacement) {
    return target.split("foo").join(replacement);
  }
}

"barfooqux".replace(FooReplace, "qux"); //'barquxqux'

class StringReplace {
  constructor(str) {
    this.str = str;
  }

  [Symbol.replace](target, replacement) {
    return target.split(this.str).join(replacement);
  }
}

"barfooqux".replace(new StringReplace("foo"), "qux"); //'barquxqux'

该符号特性与 Symbol.match 和 Symbol.replace 一样,Symbol.search 函数接收一个参数,为调用 search 方法的字符串实例

javascript
class FooSearch {
  static [Symbol.search](target) {
    return target.indexOf("foo");
  }
}

"foo".search(FooSearch); //0

class StringSearch {
  constructor(str) {
    this.str = str;
  }

  [Symbol.search](target) {
    return target.indexOf(this.str);
  }
}

"barfoo".search(new StringSearch("foo")); //3
Symbol.species

该符号作为一个属性:一个函数值,该函数作为创建派生类对象的构造函数

该符号在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法

用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建的实例的原型定义

javascript
class Bar extends Array {}
class Baz extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

let bar = new Bar();
bar instanceof Bar; //true
bar instanceof Array; //true
bar = bar.concat("bar");
bar instanceof Bar; //true
bar instanceof Array; //true

let baz = new Baz();
baz instanceof Baz; //true
baz instanceof Array; //true
baz = baz.concat("baz"); //concat会创建一个baz对象的副本
baz instanceof Baz; //false
baz instanceof Array; //true
Symbol.split

这个方法与前面 Symbol.match 等三个方法很类似,Symbol.split 方法接收一个参数,就是调用 split()方法的字符串本身

javascript
class FooSpliter {
  static [Symbol.split](target) {
    return target.split("foo");
  }
}

"afoob".split(FooSpliter); //['a', 'b']

class StringSpliter {
  constructor(str) {
    this.str = str;
  }

  [Symbol.split](target) {
    return target.split(this.str);
  }
}

"afoob".split(new StringSplit("foo")); //['a', 'b']
Symbol.toPrimitive

该属性表示:一个方法,该方法将对象转换为相应的原始值,由 ToPrimitive 抽象操作使用

该函数接收提供的参数(Number、String 或 default),可以控制返回值的原始值

javascript
class Foo {}
let foo = new Foo();

console.log(3 + foo); //"3[object Object]"
console.log(String(foo)); //"[object Object]"

class Bar {
  constructor() {
    this[Symbol.toPrimitive] = function (hint) {
      switch (hint) {
        case "number":
          return 3;
        case "string":
          return "string bar";
        default:
          return "default bar";
      }
    };
  }
}

console.log(3 + foo); //"3default bar"
console.log(String(foo)); //"string bar"
Symbol.toStringTag

该属性表示:一个字符串,该字符串用于创建对象时默认的字符串描述,由内置方法 Object.prototype.toString()使用

javascript
class Foo {}
let foo = new Foo();
console.log(foo); //Foo()
console.log(foo.toString()); //[object Object]
console.log(foo[Symbol.toStringTag]); //undefined

class Bar {
  constructor() {
    this[Symbol.toStringTag] = "bar";
  }
}
let bar = new Bar();
console.log(bar); //Foo()
console.log(bar.toString()); //[object bar]
console.log(bar[Symbol.toStringTag]); //bar
Symbol.unscopables(与 with 一同不建议使用)

该属性表示:一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除

设置该属性并绑定属性的键值为 true 就可以开启

javascript
let o = {foo: 'bar'};
whith(o) {
    console.log(foo);	//bar
}

o[Symbol.unscopables] = true;

with(o) {
    console.log(foo);	//ReferenceError
}

因为不推荐使用 with,所以也不推荐使用 Symbol.unscopables

引用值:array Object function ......

var a = [1,2,3,4,5,false,"abc"]; 指针指向,a 其实是指向数组对象的一个指针

引用值为堆内存中存储

值的改变和复制

javascript
var num = 100;
var num1 = num;
num = 200;

image-20210123171719625image-20210123171742829

javascript
var arr = [1, 2];
var arr1 = arr;
var arr = [1, 3];

image-20210123172321184

image-20210123172331716

image-20210123172509792

类型转换

var num = 1 * "1";
var num = "1" * "2";
var num = "2" - "1";
var num = "2" / "1";
var num = "1" + "1";
类型	   值
number : 1
number : 2
number : 1
number : 2
string : "11"

出现这样的原因是因为 JavaScript 有自带的类型转换

1、Number(obj):将内容转换成数字

var num = Number("123"); 123

var num = Number(true/false); 1/0

var num = Number(null); 0

var num = Number(NaN); NaN

var num = Number(undefined); NaN

var num = Number("a"); NaN

var num = Number("123abc"); NaN

var num = Number([]); 0

var num = Number(""); NaN

var num = Number("0xA"); 10

2、parseInt(obj,16(设定当前 obj 的进制,将转换为 10 进制输出,范围是 2~36)):将数转换成整型,遇到非数字位截至,并返回前面的数

var num = parseInt("123"); 123

var num = parseInt(true/false); NaN/NaN

var num = parseInt("123.3"); 123

var num = parseInt(10,16); 16

var num = parseInt(10,0); 10/NaN

var num = parseInt(3,2); NaN

var num = parseInt("100px"); 100

var num = parseInt(""); NaN

var num = parseInt("0xA"); 10

3、parseFloat():转换成浮点数,遇到除了第一个”.“以外的非数字位截至并返还

var num = parseInt("100.1px"); 100.1

4、String():转换成 string 类型

如果值有toString()方法,则调用它,如果值是 null 则返回”null“,如果是 undefined 则返回”undefined“

var str = String(undefined); "undefined"

5、Boolean():转换成布尔类型

undefined、null、NaN、“”、0、false ==> false

6、toString():将 obj 转换成 string,但是 undefined、null 不能用 toString()

javascript
var num = 123;
var str = num.toString();
("123");

toString(radix):以 10 进制为基础转换成 radix 进制

javascript
var num = 123;
var str = num.toString(8);
("173");

7、隐式类型转换:

isNaN():先将内容调用对象的valueOf()方法,确定返回值是否能转换为数值(Number()方法),如果不能,再调用toString()方法并测试其返回值

var res = isNaN(NaN); true

var res = isNaN(123) false

var res = isNaN("123") false

var res = isNaN("abc") true

var res = isNaN(null) false

++、--:先调用 Number(),再进行计算,值为 number 类型

var a = "123"; a++; 124

var a = "abc"; a++; NaN

+、-:一元正负

var a = +"abc"; number:NaN

+:加号

*、/、-、%:数学运算

&&、||、!:逻辑运算符

<、>、<=、>=:比较运算符

==、!=:判断运算符

javascript
undefined > 0;	false
undefined < 0;	false
undefined == 0;	false
undefined != 0;	true
null > 0;	false
null < 0;	false
null == 0;	false
null != 0;	true
undefined == null;	true
NaN == NaN;	false

//undefined和null不与数字进行比较,<、>、==、<=、>=输出结果全是false,!=输出结果为true

//引用值比较的是地址
{} == {} false
var obj = {}; obj1 = obj;
obj == obj1 true
obj === obj1 true

===、!==:绝对判断,不发生类型转换

NaN === NaN; false

在未定义的情况下,所有类型都为 undefined

typeof 返回值的类型都为字符串类型

操作符

一元操作符

递增/递减操作符

++、--,前缀版和后缀版有细小差异,与 c 语言相同,前缀版先自增(减)后运算,后缀版先运算后自增(减)

如果变量的值是字符串有效的数值形式,则先转为数值再进行改变,类型转为数值。

如果变量的值是字符串无效的数值形式,则将变量的值设置为 NaN,类型转为数值。

如果是布尔值 false,则设置为 0 再改变,类型转为数值。

如果是布尔值 true,设置为 1 再转变,类型转为数值。

如果是浮点值,则加 1 或减 1。

如果为对象,则调用其 valueOf()方法取得可以操作的值,对得到的值应用上述规则,如果是 NaN,则调用 toString()并再次应用其他规则,类型转为数值。

一元加和减

+、-,一元加放在数值没区别,减放在数值前将会将数值变成负值。

但两者如果运用到非数值时,将会先执行与 Number()函数一样的类型转换

位操作符

ECMAScript 所有的数值采用的是 IEEE 754 64 位格式储存,但会先把值转为 32 位整数,再进行操作,最后再把结果转为 64 位。对开发者而言,64 位整数储存格式是不可见的。

如同计组讲的,有符号整数采用最左一位充当符号位,负数采用其正数的补码(二补数)表示

将负数转换为二进制时,转换过程中会求得二补数,然后再以更符合逻辑的形式表示。

javascript
let num = -18;
num.toSring(2); //-10010

ECMAScript 中基本所有整数都为带符号的 32 位整数,不过也有无符号数。

ECMAScript 中对数值应用位操作符时。后台:64 位数值 -> 32 位数值 -> 操作 -> 64 位数值,这个转换导致了一个奇特的副作用,就是 NaN 和 Infinity 在位操作中都会被当作 0 处理;位操作处理非数值时,会先用 Number()函数将其转换为数值再应用位操作

按位非

~:返回数值的一补数

javascript
let num1 = 25; //00000019
let num2 = ~num1; //ffffffe6
num2; //-26

效果上看,按位非就是将数值取反再减一,但是它比-num1 - 1要快得多,因为它是在数值底层表示上完成的

按位与

&:将两个数的每一位对齐,然后对每一位执行相应的操作

1-1:1、1-0:0、0-1:0、0-0:0(全为 1 时返回 1)

25 & 3; //1

按位或

|:与按位与一样

1-1:1、1-0:1、0-1:1、0-0:0(至少一个为 1 时返回 1)

25 | 3; //27

按位异或

^:与上述相同

有且仅有一位为 1 时返回 1

25 ^ 3; //26

左移

<<:整个二进制数向左移,以 0 填充空位,不影响符号位

2 << 5; //64

右移

>>:与左移相同,不过会用符号位的值填充多出来的空位

64 >> 5; //2

无符号右移

>>>:将符号位算作整个数中

所以对于正数,它和右移相同,对于负数,它会将负数看作正数(因为符号位被当作数值位),然后右移,空位补 0

-64 >>> 5; //134217726

布尔操作符
逻辑非

!:按照操作数布尔值转换后的值取反即可

如:!NaN; //true

逻辑与

&&:特性看下方

逻辑或

||:特性看下方

逻辑或和逻辑与创造出来的短路语句等特性

undefined、null、NaN、“”、0、false ==> false

&&:先查看第一表达式的布尔值是否为真,如果为真,就继续看第二个表达式转换为布尔值的结果,如果只有两个表达式,只看到第二个表达式就返回该表达式的值,简称”碰到假就停“,如:

var a = 1 && 2; a = 2

var a = 0 && 1; a = 0

借着这种特性我们可以这么编程:2 > 1 && document.write('abc');(短路语句)

再扩展一下,通常前端会收到后端的传值,但是我们一般要进行判断该值是否有效,所以我们可以如此:

javascript
var data = ...;
data && function(data); (data && 执行语句)

||:当有表达式为真时,直接返回,简称”碰到真就停“

利用这个特性,当我们面对从多个内容中取出有内容的一项时,可以如下:

var event = e || window.event;

乘性操作符
乘法操作符

*:乘法,非数值操作数会先用 Number()将其转换为数值

注意:

如果有一个数为 NaN,则返回 NaN

如果 Infinity 乘以 0,返回 NaN

如果 Infinity 乘以非零有限数,则返回 Infinity 或-Infinity

如果 Infinity 乘以 Infinity,则返回 Infinity

除法操作符

/:除法,非数值操作数会先用 Number()将其转换为数值

注意:

如果除法无法表示商,则用 Infinity 或-Infinity 表示

如果有一个数位 NaN,则返回 NaN

如果 Infinity 除以 Infinity 则返回 NaN

如果 0 除以 0,返回 NaN

如果非零有限数除以 0,则返回 Infinity 或-Infinity

如果 Infinity 除以任何数值,则返回 Inifinity 或-Infinity

取模操作符

%:取模,非数值操作数会先用 Number()将其转换为数值

注意:

无限值模有限值,则返回 NaN

有限值模 0,则返回 NaN

Infinity 模 Infinity,则返回 NaN

有限值模无限值,则直接返回该有限值

0 模非零,则返回 0

指数操作符

**:与 Math.pow()一样

2**3; //8

加性操作符
加法操作符

+:加法

注意:

有任一操作数为 NaN,则返回 NaN

符号相同的无穷数相加就等于他们自己

符号不同的无穷数相加,则返回 NaN

+0 + -0,则返回+0

如果有一个操作数是字符串,则确保两个操作数都为字符串后再进行字符串拼接

字符串相加

在字符串前的数字会被进行加法运算,之后的会进行字符拼接,但是,只要是加法运算中出现字符串,该运算结果将会是子符串,如:

var s = 1 + 1 + "a" + 1 + 1; result:"2a11"

一般咋们返回的值 “NAN” 全称是 “Not A Number”

减法操作符

-:减法

注意:

符号相同的无穷数相减,则返回 NaN

符号相异的无穷数相减,就是后一操作数取反后相加,表现上是返回第一个无穷数

+0,-0 之间的减法也是将后一操作数取反后相加,按照加法准则

如果有任一操作数为字符串,则先调用 Number()方法再进行操作

如果有任一操作数为对象,则先调用 valueOf()方法,如果没有 valueOf()方法,则调用 toString()方法

关系操作符

<、>、<=、>=

注意:

如果操作数都是字符串,则比较字符编码

任一操作数为数值,则将另一操作数转为数值再进行比较

如果有任一操作数为对象,则先调用 valueOf()方法,若无,则调用 toString()方法,在进行比较

任一操作数为布尔值,则先转换为数值再进行比较

任一操作数为或者转为 NaN,则返回 false

相等操作符
相等和不相等

==、!=

注意:

任一操作数为布尔值,则转换为数值再比较

一字符串一数值,将字符串转为数值再比较

一对象一非对象,则调用对象的 valueOf()方法,再比较

null 和 undefined 相等

null 和 undefined 不能被转换为其他类型值进行比较

任一操作数为 NaN,相等操作返回 false,不等操作返回 true,即使两个都为 NaN 也是一样,因为规定 NaN 不等于 NaN

都为对象,则比较是否为同一对象,同则返回 true,不同返回 false

全等和不全等

===、!==

注意:

将会取消类型转换,以操作数原本的数据类型进行比较

如:"55" === 55; //falsenull === undefined; //false(数据类型不相同)

条件操作符
三目运算符
javascript
1 > 0 ? 2 + 2 : 1;  =>  4
var num = 1 > 0 ? ("10" > "9" ? 1 : 0) : 2;  =>  0
赋值操作符

=、+=、-=、*=、/=、%=、<<=、>>=、>>>=

仅仅只是简写语法,并不会提升原语法性能

逗号操作符

,:在一条语句中执行多个操作let num = 2, mun1 = 1;,用来辅助赋值,返回表达式中最后一个值let num = (1, 2, 0); //num = 0;

语句

if 语句

会自动调用 Boolean()将括号里的条件表达式转换为布尔值

还有 else 和 else if

循环语句

do-while 语句

先执行后判断

while 语句

先判断后执行

for 语句

初始化代码中,初始化、条件表达式和循环后表达式都是不必须的

巧用方法 var i = 100; for(; i-- ;){}

如果使用 var 定义迭代变量,这个变量会渗透到循环体外部,但使用 let 不会。

同时,在执行超时逻辑时:如果用 var 定义变量,则在退出循环时,迭代变量保存的是退出循环的值,因此所有的 i 都将会是同一个变量,输出的自然是同一个值;但如果是使用的 let 声明的话,JavaScript 引擎会在后台为每个迭代循环声明一个新的迭代变量,例如:

javascript
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// 5 5 5 5 5

for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2 3 4

当然,这种每次迭代声明一个独立变量的实例行为适用于所有的 for 循环,包括 for-in、for-of

但是 const 不能用于声明变化的迭代变量,因为迭代变量会自增,不能执行类似于i++

for-in 语句

for (const i in obj) {}

严格的迭代语句,用于枚举对象中非符号键属性,因为对象属性是无序的,所以返回的对象属性的顺序不能保证,因浏览器而异,如果 for-in 的对象是 null 或 undefined 则不执行循环体

javascript
let arr = { hello: "1" };
for (let i in arr) {
  console.log(i);
}
//hello

for-in 遍历数组时(不建议用 for-in 迭代数组),i 代表的是 index

javascript
let arr = [1, 444, 3, "shuzu"];
for (let i in arr) {
  console.log(i);
}
//0 1 2 3
for-of 语句

严格的迭代语句,用于遍历可迭代对象的元素

for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素,如果变量不支持迭代,则 for-of 会抛出错误

标签语句

标签:代码

start: for (let i = 0; i < 10; i++) {};,需要配合 break 或 continue 来使用

break 和 continue 语句

break 和 continue 不必多说,其实 js 里可以和标签语句配合使用,例如:

javascript
let num = 0;
start: for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (...) {
            break start;
        }
    }
}

console.log("hello");

这个例子中,满足了 break 条件后,break 将退出两层循环,直接进行 start 所指定循环后的代码,也就是打印 hello 的语句

continue 也一样

javascript
let num = 0;
start: for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (...) {
            continue start;
        }
    }
    console.log("bye");
}

console.log("hello");

这里会退出第二层循环,然后继续执行 start 所指定的外部循环,相当于在外层循环中使用了 continue,内层循环后的代码也不会执行了,也就是本次不会打印 bye 了

with 语句

改变 with 代码块中的作用域,后文作用域有提及,with 在严格模式下不允许使用,而且本身 with 就不建议被使用

switch 语句

javascript
switch () {
	case :
        //....
        break;
    case :
        //....
        break;
	default :
        //....
}
//注意每个case里的break,如果不加break,将会把以下所有case全部执行完。default位置不一定在最后,但是switch如果能够看到default将会直接执行。

ECMAScript 赋予 switch 语句新的特性,就是 case 判断可以使用表达式,因为 case 会比较每个条件值,而且是使用全等判断,如case "10":如果你传入的是数值10则不会进入此 case

存储类型

数组[]、对象{}

对象由键值对组成,属性名对应一个属性值

javascript
var obj = {
	name : "lsn",
	age : 19,
    none : undefined,
    handsome : true,
}
取值、赋值:
var name = obj.name;
obj.name = "lll";

鉴别数据类型

typeof()

javascript
var num = 123;
console.log(typeof num);
number;
var num = "123";
console.log(typeof num);
string;
//如此typeof()可以返回六个不同的值:
//number、string、boolean、object、undefined、function、symbol(es6新增)
//null、[]都属于object
//不加空格也可以用,如:typeof num;

所有实现了内部[[Call]]方法的对象在用 typeof()方法时都会返回”function“,所以对于正则表达式,某些浏览器返回”function“,某些返回”object“

自定义 type 函数

javascript
function type(target) {
  var ret = typeof target;
  var template = {
    "[object Array]": "array",
    "[object Object]": "object",
    "[object Number]": "number - object",
    "[object Boolean]": "boolean - object",
    "[object String]": "string - boolean",
  };
  if (target === null) {
    return "null";
  } else if (ret == "object") {
    var str = Object.prototype.toString.call(target);
    return template[str];
  } else {
    return ret;
  }
}

instanceof(鉴别引用值类型,由原型链决定)

javascript
finction Person() {

}
var person = new Person();

//A对象是不是B构造函数构造出来的
//也就是instanceof 会看A对象的原型链上有没有B的原型!!!
A instanceof B

> person instanceof Person
< true
> person instanceof Object
< true
> [] instanceof Array
< true
> [] instanceof Object
< true
var obj = {}
> obj instanceof Array
< false

所有的引用值都是 Object 的实例,所有的原始值都不是对象,所以用 instanceof 检测原始值都会返回 false

利用 toString call

javascript
Object.prototype.toString.call([]);
//Object.prorotype.toString = function () {
//this  谁调用的这个函数,这个this就指向谁
//}

image-20210520195701014

image-20210520195857197

函数 Function

函数实际上是对象,每个函数都是 Function 类型的实例,而 Function 也有属性和方法,和其他引用类型一样

可以将函数名想象成指针,函数想象成对象

注意,严格模式下函数有以下规定:

函数不能以 eval 或 arguments 作为名称,同样他们俩也不能做参数名,函数参数不能同名

javascript
//函数声明
//js引擎会在任何代码执行之前,先读取函数声明并在执行上下文中生成函数定义,叫做“函数声明提升”
function test(a, b) {}

//函数体
//必须等到代码执行的那一刻才会在执行上下文中生成函数定义,用var和let都是这样
//1、命名函数表达式
var test = function test() {};
//2、匿名函数表达式 --> 函数表达式
var test = function () {};

//箭头函数
let test = () => {};

//Function构造函数(不推荐)
//接收任意多个字符串参数,最后一个参数始终会被当成函数体,之前的参数都是函数参数
let test = new Function("arg1", "arg2", "return arg1 + arg2");
//这段代码会被解释两次:第一次将它当作常规ECMAScript代码,第二次解释传给构造函数的字符串

**return:**终止函数、返回值

**作用域:**变量和函数生效(能被访问的)的区域

没有重载

同一个函数被定义多次,那么,最后一个会覆盖之前所有的定义

箭头函数

任何可以使用函数表达式的地方,都可以使用箭头函数

箭头函数非常适合嵌入式场景,因为其简洁的语法

只有没有参数或者多个参数的情况下,才需要使用括号

箭头函数也可以不用大括号,如果不使用大括号,箭头后面只能由一行代码,例如一个赋值操作、或者一个表达式,而且会隐式返回这行代码的值

**注意:**箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数,更没有 prototype 属性

函数名

ECMAScript6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。大多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名,即使函数没有名称也会如实显示成空字符串。如果他是 Function 构造函数构建的,则会标识成“anonymous”

如果是一个获取函数、设置函数,或者使用 bind()实例化,那么标识符前面会加上相应的前缀bound foo、get foo、set foo

bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用

js
const module = {
  x: 42,
  getX: function () {
    return this.x;
  },
};

const unboundGetX = module.getX;
console.log(unboundGetX());
// undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// 42

bind() 函数会创建一个新的绑定函数bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数绑定函数具有以下内部属性:

  • [[BoundTargetFunction]] - 包装的函数对象
  • [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
  • [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
  • [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。

当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 [[Call]],就像这样 Call(boundThis, args)。其中,boundThis[[BoundThis]]args[[BoundArguments]] 加上通过函数调用传入的参数列表

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数

bind 的用法
创建绑定函数

bind() 最简单的用法是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。JavaScript 新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,期望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象。基于这个函数,用原始的对象创建一个绑定函数,巧妙地解决了这个问题:

js
this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function () {
    return this.x;
  },
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的

// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
偏函数

bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面

js
function list() {
  return Array.prototype.slice.call(arguments);
}

function addArguments(arg1, arg2) {
  return arg1 + arg2;
}

var list1 = list(1, 2, 3); // [1, 2, 3]

var result1 = addArguments(1, 2); // 3

// 创建一个函数,它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var list2 = leadingThirtysevenList();
// [37]

var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]

var result2 = addThirtySeven(5);
// 37 + 5 = 42

var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略
配合 setTimeout

在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window(或 global)对象。当类的方法中需要 this 指向类的实例时,你可能需要显式地把 this 绑定到回调函数,就不会丢失该实例的引用

作为构造函数使用的绑定参数(不是最佳解决方案)
js
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return this.x + "," + this.y;
};

var p = new Point(1, 2);
p.toString(); // '1,2'

var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /*x*/);

// 本页下方的 polyfill 不支持运行这行代码,
// 但使用原生的 bind 方法运行是没问题的:

var YAxisPoint = Point.bind(null, 0 /*x*/);

/*(译注:polyfill 的 bind 方法中,如果把 bind 的第一个参数加上,
即对新绑定的 this 执行 Object(this),包装为对象,
因为 Object(null) 是 {},所以也可以支持)*/

var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'

axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new YAxisPoint(17, 42) instanceof Point; // true
快捷调用

在你想要为一个需要特定的 this 值的函数创建一个捷径(shortcut)的时候,bind() 也很好用

js
var slice = Array.prototype.slice;
slice.apply(arguments);

// 你可以这么写

// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);
slice(arguments);

因为这样一来 Function.prototype.apply() 绑定的 this 就会是 Array.prototype.slice()

参数、arguments

实参在函数里被存在列表 arguments(类数组对象)里,形参和实参没有强制规定个数

javascript
function abc(a) {
  console.log(arguments);
}
abc(1, 2, 3);
arguments = [1, 2, 3]; //实参列表

在形参和列表 arguments 存在一种映射,某个值改变相应的另一个值也改变

但是形参和 arguments 在内存中是分开的

javascript
function sum(a, b) {
  a = 2;
  console.log(arguments[0]);
  arguments[0] = 4;
  console.log(a);
}
sum(1, 3);

2;
4;

但是如果函数一开始就没有存在的映射并不会有此效果

javascript
function sum(a, b) {
  b = 2;
  console.log(arguments[1]);
}
sum(1); //只传一个实参情况

undefined;

严格模式下,给 arguments 赋值不会再影响形参的值;在函数中尝试重写 arguments 对象会导致语法错误

javascript
function sum(a, b) {
  a = 2;
  console.log(arguments[0]);
  arguments[0] = 4;
  console.log(a);
}
sum(1, 3);

1;
2;
arguments.callee

指向 arguments 对象函数所在的指针

javascript
function test(num) {
  if (num < 1) {
    return 1;
  } else {
    return num * test(num - 1);
  }
}
//这样只能调用名称为test的函数,如果函数名变化就会出现相应问题
let trueTest = test;
test = function () {
  return 0;
};
test(5); //0
trueTest(5); //0,因为trueTest函数里会调用test()

//下面可以避免这种情况
function test(num) {
  if (num < 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
let trueTest = test;
test = function () {
  return 0;
};
test(5); //0
trueTest(5); //120

默认参数值

es5.1 以前需要手动检测某个参数是否为 undefined,es6 之后就能显式定义默认参数了

但是这样将会断开形参与 arguments 之间的同步映射(只要有一个形参有默认值就会这样)

javascript
function test(name = "lsn") {
  console.log(name);
  console.log(arguments[0]);
}
//给函数传undefined相当于没有传值,也就是arguments不会和name建立联系
test(undefined);
//lsn
//undefined

function test(name = "lsn", say) {
  name = "haha";
  say = "lll";
  console.log(arguments[0], arguments[1]);
}
test("foo", "l");
//foo

默认参数值不一定是原始值或对象类型,可以是调用函数后的返回值function test(name = getName()) {}

这个 getName()求值函数只有在 test()函数被调用的时候才会运行求值,test()函数定义时不会

箭头函数也能使用默认值,不过在只有一个参数时就必须加括号了

默认参数作用域与暂时性死区

先定义的默认参数可以被后面的命名参数使用,但是不能被前面的命名参数使用,这里和按顺序 let 定义 let 是一样的

javascript
function test(name = "lsn", val = "foo") {}
//同下
function test() {
  let name = "lsn";
  let val = "foo";
}

function test(name = "lsn", val = name) {}
//这里val就等于lsn

//参数有自己的作用域,所以不能使用函数体的作用域
//function test(name = hi) {
//    let hi = 'lsn';
//}
//上面这种会报错

扩展参数

使用扩展操作符会将可迭代对象拆分,并将迭代返回值每个单独传入

javascript
function test() {
  console.log(arguments.length);
}
test([0, 1]); //1
test(...[0, 1]); //2

收集参数

正好和扩展参数相反,会得到一个 Array 实例

但是不影响 arguments

javascript
function test(...value) {
  console.log(value);
}
test(1, 2, 3); //[1, 2, 3]
//Arguments(3) [1, 2, 3, callee: (...), Symbol(Symbol.iterator): ƒ]

this

在标准函数中,this 引用的是把函数当成方法调用的上下文对象(如在全局调用函数时,this 指向 window)

在箭头函数中,this 引用的是定义箭头函数时的上下文,而且因箭头函数默认没有自己的 this,而是会像闭包一样返回外层的 this(像是沿着作用域链向上找到箭头函数定义时所在的词法作用域所指向的 this),并且箭头函数的 this 是不可改的

javascript
window.color = "red";
let o = {
  color: "blue",
};

function say() {
  console.log(this.color);
}
say(); //red
o.say = say;
o.say(); //blue

let say1 = () => {
  console.log(this.color);
};
say1(); //red
o.say1 = say1;
o.say1(); //red

传递参数

因为原始值和引用值存储方式不一样,函数中传参方式有区别,原始值为按值传递,即复制一份副本传入到参数中;而引用值则会将引用值的堆内存位置复制到参数中。

两者其实和复制值是一样的,上文有解释

arguments(ES5 严格模式下不允许使用)

1、callee

能够调用当前 function(严格模式下访问会报错)

javascript
function test() {
  console.log(arguments.callee);
}

test();

image-20210520234245906

阶乘

javascript
var num = (function (n) {
  if (n == 1) {
    return 1;
  }
  return n * arguments.callee(n - 1);
})(100);
2、caller

es5 会给函数对象上添加这个属性,基本浏览器早期版本都支持这个属性

引用当前函数被调用的环境,如果是全局作用域中调用则为 null

javascript
function test() {
  demo();
}

function demo() {
  console.log(demo.caller);
}

test();

image-20210521103642552

如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值

new.target

在函数里调用,如果该函数是使用 new 关键字调用的,则 target 会引用被调用的构造函数,如果该函数被当成普通函数运用,则返回 undefined

javascript
function test() {
  if (new.target) {
    throw "hello";
  }
}
new test(); //hello
test(); //

属性

每个函数都有两个属性:length 和 prototype

length:该属性保存函数定义的命名参数的个数

javascript
function test(helo) {}
test.length; //1

prototype:该属性不可枚举,所以使用 for-in 不会返回这个属性

方法

apply()、call()

这两个方法可以改变函数内 this 的引用,下文有详细说明

javascript
fun.apply(obj, arguments); //可以是Array实例也可以是arguments对象
fun.call(obj, ...arguments); //参数必须一个个分开

在严格模式下,调用函数如果没有指定上下文对象,则 this 值不会指向 window。除非使用 apply()或 call()把函数指定给一个对象,否则 this 值会变成 undefined

函数表达式

任何时候,把函数当作值来用,他就是一个函数表达式

递归

阶乘

javascript
function mul(n) {
  if (n == 1) {
    return 1;
  }
  return n * mul(n - 1);
}

斐波那契数列

javascript
function fb(n) {
  if (n == 1 || n == 2) {
    return 1;
  }
  return fb(n - 1) + fb(n - 2);
}

但是这样直接用函数名可能会有问题,因为如果这个变量被赋值为 null 则会报错,所以建议使用 arguments.callee

但是在严格模式下,不能访问 arguments.callee,所以这里可以用命名函数表达式

javascript
let f = function ff(num) {
  if (num <= 1) return 1;
  else return num * ff(num - 1);
};
f(3); //6

即使把函数值赋给另一个变量,函数表达式的名称 ff 也不变

尾调用优化

es6 规定如果一个函数的返回时另一个函数的返回,则执行尾调用优化,具体如下

javascript
function outter() {
    return inner();	//尾调用
}

上述代码在es6中优化如下:
1、执行到outter函数体,第一个栈帧被推到栈上
2、执行outter函数体,到达return语句。为求值语句,必须先求值inner
3、引擎发现把第一个栈弹出栈外也没关系,因为inner的返回值也是outter的返回值
4、弹出outter的栈帧
5、执行到inner函数体,栈帧被推到栈上
6、执行inner函数体,计算其返回值
7、将inner的栈帧推出到栈外

现在的浏览器没法测试尾调用优化是否起作用,但是这个是 es6 规范所规定的

尾调用优化条件

代码在严格模式下执行、外部函数的返回值时对尾调用函数的调用,尾调用函数返回后不需要执行额外的逻辑、尾调用函数不是引用外部函数作用域中自由变量的闭包

尾调用优化很适合递归,因为递归代码最容易在栈内存中产生大量栈帧

之所以要求严格模式,主要是因为非严格模式下函数调用允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧

立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)

定义后立即执行,执行完立即释放,不占用内存

javascript
(function () {
  //...
})();
(
  (function abc() {
    //...
  })()
);
(function abc() {
  //...
})();
//W3C建议前两种执行方式
//只有表达式才能被执行符号"()"执行,function test(){}();这种将不会执行,因为()前方是函数定义式;var test = function (){}();这种将会执行,并且和立即执行函数没有区别
//被执行符号执行的表达式将会自动放弃函数的名称
//+ function (){}();这种将会执行,+ - !等都可以
//()可以是数学优先表达式所以(function (){})()、(function (){}())首先是数学表达式()将函数定义式转化为表达式,然后就可被执行

如果传参(
  (function abc(a, b) {
    //...
  })(a, b)
);

如果要返回值;
var abc = (function abc(a, b) {
  //...
  return 12;
})(a, b);
ali 笔试题
javascript
function test(a, b, c, d) {
  //...
}
1, 2, 3;
//这样子将不会报错
//因为系统会认为你是
function test(a, b, c, d) {
  //...
}

1, 2, 3;
//俩部分,前一部分是函数定义式,后一部分为表达式(‘,’也算是运算符)

在 es5.1 以前,为了防止定义的变量外泄,常常用 IIFE,但是 es6 以后就可以用块级作用域了

javascript
//内嵌块级作用域
{
  let i;
  for (i = 1; i < count; i++) {
    console.log(i);
  }
}
console.log(i); //报错

//循环块级作用域
for (let i = 0; i < count; i++) {
  console.log(i);
}
console.log(i); //报错

私有变量

私有变量和特权方法、静态私有变量、模块模式模块增强模式(详情请看红宝书 p316)

预编译:在函数执行的前一刻发生(生成函数上下文)

1、创建 AO(Activation Object)对象 [翻译:执行期上下文]

2、找形参和变量声明,将变量和形参名作为 AO 的属性名,值为 undefined

3、将实参和形参相统一

4、找函数体的函数声明,赋值于函数体

5、创建 arguments 和 this,这个 this 指向 window()

javascript
function fn(a){
    console.log(a);
    var a = 123;
    console.log(a);
    function a(){}
    console.log(a);
    var b = function (){}
    console.log(b);
    function d(){}
}
fn(1);

//step1
    AO{

    }
//step2
    AO{
        a:undefined,
        b:undefined,
    }
//step3
    AO{
        a:1,
        b:undefined,
    }
//step4
	AO{
        a:function a(){},
        b:undefined,
        d:function d(){},
    }
 //step5
     AO{
         arguments:[],
         this:window,
         a:function a(){},
         b:undefined,
         d:function d(){},
     }

//执行函数体(控制台打印)
f a(){}
123
123
f (){}

全局对象没有形参,所以没有第三步,而且第一步创建的是 GO(Global Object)对象,

GO === window

javascript
console.log(a);
var a = 123;
//控制台输出:undefined

console.log(a);
//控制台输出:error: a is not defined

test();
function test() {
  console.log("test");
}
//控制台输出:test

程序优先找自己所拥有的变量,在函数中优先 AO 中的对象,如果没有则向父级寻找,例如 GO

**注意:**预编译不管有没有 if ,只看声明

javascript
console.log(a); //undefined
console.log(c); //undefined
if(){
   var a;//会被预编译
   function c(){}//会被预编译
}

最新:亲自实验 IE、chrome、Edge,发现新特性!!!

if 里面的 function f 函数声明会在 GO 和这个函数里面的 AO 同时声明 f = function(),这是 if 语句里的特性

javascript
var a;
console.log(f);
if (true) {
    function f () {
        console.log(f);
        f = a;
        console.log(f);
        //console.log("test");
    }
    console.log(f);
}
f();
console.log(f);

//console
undefined
ƒ f () {
    console.log(f);
    f = a;
    console.log(f);
    console.log("test");
}
ƒ f () {
    console.log(f);
    f = a;
    console.log(f);
    console.log("test");
}
undefined
ƒ f () {
    console.log(f);
    f = a;
    console.log(f);
    console.log("test");
}

作用域

每个 javascript 函数都是一个对象,对象中有些属性我们可以访问,有些不可以(仅供 javascript 引擎提取,[[scope]]就是其中一个)。[[scope]]就是我们所说的作用域,其中存储了执行期上下文对象的集合,这个集合呈链式链接,被称为作用域链。

作用域链是栈式结构

当函数执行时,会创建一个执行期上下文,多次创建会产生多个执行期上下文,每个都是独一无二的,函数结束时将会被会销毁。

javascript
function a() {
  function b() {
    var b = 234;
  }
  var a = 123;
  b();
}
var glob = 100;

image-20210506200618763

image-20210506201242335

b 函数被创建时拿到的 a 函数的 AO 对象就是 a 函数的 AO 对象的引用

image-20210506201956137

image-20210506202022969

作用域链增强

执行上下文有全局上下文和函数上下文两种(eval()调用内部存在的第三种上下文),但有方式可以增强作用域链,即某些语句会在作用域前端临时添加一个上下文,这个上下文在代码执行后会被删除

有两种情况会出现:try-catch 语句的 catch、with

对于 with 来说是添加指定对象,对 catch 来说是创建一个新的变量对象,这个变量对象包含要抛出的错误对象的声明

with

with 会将传入的参数当作 with 块里的作用域链的最近的 AO(即作用域链顶端)

但是,正因为 with 能改变作用域链,这将会消耗大量资源,所以在 es5.0 中不允许被使用

javascript
var obj = {
  name: "obj",
};

var name = "window";

function test() {
  var name = "scope";
  with (obj) {
    consoel.log(name);
    // --> obj --> AO:test --> GO:window
  }
}
test();
with 运用
javascript
//当我们要重复用某个对象上的方法时,可以利用with

with (document) {
  write("a");
  //....
}

如果 with 中使用 var 来定义变量,这个变量将会成为函数上下文中的一部分,而 let 则不会。

javascript
function test() {
  let hello = "j";
  with (location) {
    var h = hello + href;
  }
  console.log(h);
}
test();
//这里console.log能够访问得到h,但是with里换成let后就访问不到了。

垃圾回收

JavaScript 有自动内存管理实现内存分配和闲置资源回收:确定哪个变量不会再使用了,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一段时间就会自动运行(或者说在代码执行的某个预定的收集时间),但是确定内存是否不需要却没那么容易,有时候判断内存是否释放并不是那么明显。

浏览器发展史上,用到过两个主要的标记策略

标记清理

最常用的垃圾回收策略

给变量标记以实现记录变量是否在上下文中,不在则由垃圾回收程序做内存清理,并回收他们的内存

引用计数

不常用的回收策略

每一个值记录他被引用的次数,如果为零则表示可以被回收,如果不为零则不会被回收

但是这样有一个很大的弊端,在循环引用下,内存永远不可能回收,例如:

javascript
function test() {
  let obj1 = new Object();
  let obj2 = new Object();
  obj1.some = obj2;
  obj2.some = obj1;
}

如果这个函数多次被调用,则会产生大量不能被释放的内存,如果要终端这种循环,则将obj1.some = null; obj2.some = null;

性能

垃圾回收的时间调度很重要,在某些设备上,垃圾回收可能会明显拖慢渲染速度和帧速率

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行,探测机制因引擎而异

某些浏览器能够主动触发垃圾回收机制(但不推荐

内存管理

JavaScript 运行在一个内存管理和垃圾回收机制都很特殊的环境里。分配给浏览器的内存要比分配给桌面软件的要少很多,分配给移动端的就更少,这更多的是出于安全考虑的,避免出现运行大量 JavaScript 的网页而耗尽系统内存导致操作系统崩溃的情况出现。

我们需要将内存占用量保持在一个较小的值,这样可以让页面性能更好,也就是说除了必要数据之外,我们应当将不必要的全局变量和全局对象设置为 null(这个可以叫做解除引用),从而等待下一次垃圾回收时回收它们

而且尽量多使用 let 和 const,使用他俩可能会让垃圾回收程序尽早地介入

隐藏类和删除操作(V8 引擎)

chrome 的 V8 JavaScript 引擎会在将 JavaScript 代码编译为实际机器码时利用隐藏类

V8 会将创建的对象与隐藏类关联起来,以跟踪他们的属性,能够共享相同隐藏类的对象性能会更好,例如:

javascript
function Art() {
  this.title = "same";
}

let a1 = new Art();
let a2 = new Art();
// V8会在后台配置,让两个实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型

// 如果之后又添加下面这行代码
a2.author = "lsn";
// 这时两个实例将会对应不同隐藏类,根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响
// 避免这种情况,应在构造函数中一次声明所有属性
function Art(str) {
  this.title = "same";
  this.author = str;
}

但是 delete 操作会导致生成相同的隐藏类片段

javascript
function Art(str) {
  this.title = "same";
  this.author = str;
}

let a1 = new Art();
let a2 = new Art();

delete a2.author;

这样操作会将 a1 和 a2 不再共享同一个隐藏类,最佳方案是将不想要的属性设置为 null,这样既保持隐藏类不变和继续共享,也能达到删除引用值供垃圾回收程序回收的效果

内存泄漏:实际上是内存占用

意外声明全局变量

javascript
function test() {
  name = "lsn";
}

外部闭包很常见,如

javascript
let name = "lsn";
setInterval(() => {
  console.log(name);
}, 100);
// 只要定时器一直运行,回调函数中name就会一直占用内存,垃圾回收程序将不会清理name

内部闭包

javascript
let out = function () {
  let name = "lsn";
  return function () {
    return name;
  };
};
静态分配与对象池

浏览器运行垃圾回收的一个标准是对象更替速度

javascript
function add(a, b) {
  let vc = new Vector();
  vc.x = a.x + b.x;
  vc.y = a.y + b.y;
  return vc;
}
// 如果这个vc矢量对象生命周期很短,这个函数又被频繁调用,垃圾回收调度程序会发现这里对象更替速度很快,从而会更频繁的安排垃圾回收,所以可以改成下面这个样子
function add(a, b, vc) {
  vc.x = a.x + b.x;
  vc.y = a.y + b.y;
  return vc;
}

不过这个 vc 在哪里创建不会被垃圾回收调度程序盯上呢,答案是对象池

javascript
// vectorPool是已有的对象池
let v1 = vectorPool.allocate();
let v2 = vectorPool.allocate();
let v3 = vectorPool.allocate();

v1.x = 1;
v1.y = 1;
v2.x = 2;
v2.y = 2;

add(v1, v2, v3);

vectorPool.free(v1);
vectorPool.free(v2);
vectorPool.free(v3);

// 如果对象有属性引用了其他对象
v1 = null;
v2 = null;
v3 = null;

如果对象池只按需分配矢量(对象不存在时创建新的,对象存在时复用存在的),那么这个对象池必须使用某种结构维护所有对象,数组是个好选择,但是因为 js 中数组大小是动态可变的,所以注意不要招致额外的垃圾回收,需要在事先确定好这个数组大小。

闭包

闭包是指那些引用了另一个函数作用域中变量的函数

但凡是内部函数被保存到了外部必定会生成闭包,闭包会导致作用域链不释放,造成内存泄露

javascript
function a() {
  function b() {
    var bbb = 234;
    document.write(aaa);
  }
  var aaa = 123;
  return b;
}
var glob = 100;
var demo = a();
demo();

image-20210506205345119

闭包的作用:

可以实现共有变量(函数累加器)、可以做缓存(储存结构)、可以实现封装,属性私有化、模块化开发,防止污染全局变量

闭包问题:
javascript
function test() {
  var arr = [];
  for (var i = 0; i < 10; i++) {
    arr[i] = function () {
      document.write(i + " ");
    };
  }
  return arr;
}

var myArr = test();
for (var i = 0; i < 10; i++) {
  myArr[i]();
}

//输出结果: 10 10 10 10 10 10 10 10 10 10
解决方法:
javascript
function test() {
  var arr = [];
  for (var i = 0; i < 10; i++) {
    (function (j) {
      arr[j] = function () {
        document.write(j + " ");
      };
    })(i);
  }
  return arr;
}

var myArr = test();
for (var i = 0; i < 10; i++) {
  myArr[i]();
}

//输出结果: 0 1 2 3 4 5 6 7 8 9
经典题型
javascript
var x = 1;
if (function f() {}) {
  x += typeof f;
}
console.log(x);

//输出“1undefined”
//因为if的括号终究是括号,所以里面的函数定义式将会转化为表达式,函数等同于未声明,而且typeof函数是唯一一个不会报错的函数,f未定义,所以返回undefined,而且typeof返回的值是string型;所以和前面number:1相加就会变成字符串“1undefined”

对象 Object

javascript
var mrDeng = {
  name: "MrDeng",
  age: 40,
  sex: "male",
  health: 100,
  smoke: function () {
    console.log("I am smoking cool!!!");
    this.health--;
  },
  drink: function () {
    console.log("I am drinking");
    this.health++;
  },
};

对象创建方法

javascript
1. var obj = {}  plainObject  对象字面量/对象直接量
2. 构造函数
	1)系统自带的构造函数  new Object()
	2)自定义
var obj = new Object();

//方法1、2创建的对象没有任何区别,不过1在创建时不会实际调用Object构造函数
//javascript生产出的对象是灵活的,不同于c++和java中生成的对象是固定的

var obj = Object.create(xxx.prototype/null, 对象属性(可不传));
//三种方式一样,只不过最后一种需要传入Object或null,传入null时所构造的对象将会没有原型,而且可以传第二个参数,与defineProperties()的第二个参数一样,详情请看下文定义多个属性
//var声明的全局变量、在函数范围内声明的局部变量所增加的属性一定是不可配置的属性,例如不能进行delete操作

对象属性和方法

ECMA-262 使用一些内部特性来描述属性的特征,开发者不能在 js 中直接访问这些特性,为了将某个特性标识为内部特性,会用两个中括号将特性的名称括起来,如[[scope]]

数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置,有四个属性特性描述他们的行为

[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改他的特性,以及是否可以把它改为访问器属性,所有直接定义在这个对象上的属性都为 true,默认为 false

[[Enumerable]]:表示属性是否可以通过 for-in 循环返回,所有直接定义在这个对象上的属性都为 true,默认为 false

[[Writable]]:表示这个属性的值是否可以被修改,所有直接定义在这个对象上的属性都为 true,默认为 false

[[Value]]:包含属性的值,默认为 undefined

要修改属性的默认特性,就必须用 Object.defineProperty()方法,该方法接受三个参数

javascript
let person = {};
Object.defineProperty(person, "name", {
  configureable: true,
  enumerable: true,
  writable: false,
  value: "nico",
});
//第三个参数里可以根据要修改的特性设置一个或多个值
console.log(person.name); //"nico"
person.name = "lsn"; //writable设为false后,严格模式下修改只读属性会抛出错误
console.log(person.name); //"nico"

一个属性被设置为不可配置后,就不可能再变回可配置了,再次调用并修改任何非 writable属性会导致错误

访问器属性

访问器不包含数据值,相反,他们包含一个获取函数(getter)和一个设置函数(setter),不过这两个函数不是必须的。在读取访问器属性时会调用获取函数,返回一个有效的值,在写入访问器属性时,会调用设置函数并传入新值,访问器属性有 4 个特性:

[[Configurable]]:与数据属性一样

[[Enumerable]]:与数据属性一样

[[Get]]:获取函数,读取时调用,默认值为 undefined

[[Set]]:设置函数,写入时调用,默认值为 undefined

访问器属性必须使用 Object.defineProperty()

javascript
let person = {
  year: 2017,
  el: 1,
};
Object.defineProperty(person, "year", {
  get() {
    return this.year;
  },
  set(val) {
    this.year = val;
    this.el++;
  },
});
book.year = 2001;
book.el; //2

获取函数和设置函数不一定都要设置,只定义获取函数意味着只读,修改属性值会被忽略,严格模式下会抛出错误;同样的,只定义设置函数同理

在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]][[Enumerable]]

定义多个属性

Object.defineProperties(),接收两个参数

javascript
let person = {};
Object.defineProperties(person, {
  year: {
    value: 2017,
  },
  year_: {
    get() {
      return this.year;
    },
  },
});
读取属性的特性

Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符,接受两个参数(只对当前实例属性有效,如果想读取原型上的值,请直接对原型对象使用)

javascript
let person = {};
Object.defineProperties(person, {
  year: {
    value: 2017,
  },
  year_: {
    get() {
      return this.year;
    },
  },
});
let des = Object.getOwnPropertyDescriptor(person, "year");
console.log(des); //{value: 2017, writable: false, enumerable: false, configurable: false}
let des1 = Object.getOwnPropertyDescriptor(person, "year_");
console.log(des1); //{set: undefined, enumerable: false, configurable: false, get: ƒ}

es2017 新增了 Object.getOwnPropertyDescriptors(),这个方法实际上会在每个自由属性上调用 Object.getOwnPropertyDescriptor()并在一个新对象上返回它们

javascript
console.log(Object.getOwnPropertyDescriptors(person));

//->
{
    year: {
        configurable: false,
        enumerable: false,
        value: 2017,
        writable: false
    },
    year_: {
        configurable: false,
        enumerable: false,
        get: ƒ get(),
        set: undefined
    }
}
合并对象

Object.assign()方法(浅复制),接收一个目标对象和一个或多个源对象,然后将源对象可枚举属性(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性值,然后使用目标函数上的[[Set]]设置值

javascript
let dest, src, result;
dest = {};
src = { id: "src" };

result = Object.assign(dest, src);
dest === result; //true
dest !== src; //true
//result	=>	{id: src}

result = Object.assign(dest, { a: "foo" }, { b: "bar" });
//result	=>	{a: foo, b: bar}

dest = {
  set a(val) {
    console.log(val);
  },
};
src = {
  get a() {
    return "foo";
  },
};
Object.assign(dest, src);
//调用src的get,然后调用dest的set并传入值,但是set并没有保存值,所以值并未传过来
//dest	=>	{set a(val) {...}}

如果多个源对象都有相同的属性,则使用最后一个复制的值,从源对象访问器属性取得的值,会作为一个静态值赋给目标对象,不能在两个对象间转移获取函数和设置函数

javascript
let dest = {
    year: 2017
};
Object.defineProperty(dest, "a", {
    enumerable: true,
    set(val) {
        this.year = val;
        console.log("hello");
    },
    get() {
        console.log(this.year);
        return 2001;
    }
})
let result = {};
let res = Object.assign(result, dest);
console.log(Object.getOwnPropertyDescriptors(res));
// ->
{
    //这里的a属性由访问器属性变为数据属性
    a: {
        configurable: true,
        enumerable: true,
        value: 2001,
        writable: true
    },
    year: {
        configurable: true,
        enumerable: true,
        value: 2001,
        writable: true
    }
}

如果复制中途出错,操作会终止并抛出错误,但是在此之前的复制操作都已经完成,并不会**“回滚”**

对象相等判定

Object.is(),该方法必须接受两个参数

javascript
true === 1; //false

+0 === -0; //true
+0 === 0; //true
-0 === 0; //true

NaN === NaN; //false
isNaN(NaN); //true

Object.is(true, 1); //false
Object.is(+0, -0); //false
Object.is(+0, 0); //true
Object.is(-0, 0); //false
Object.is(NaN, NaN); //true
增强对象语法
属性值简写
javascript
let name = "lsn";
let person = {
  name: name,
};
person.name; //lsn

//简写属性名,如果没有找到同名变量则会报错
let person = {
  name,
};
person.name; //lsn

//代码压缩程序会在不同的作用域间保留属性名,以防止找不到引用
function getName(name) {
  return {
    name,
  };
}
let person = getName("lsn");
person.name; //lsn
可计算属性

不能在对象字面量中直接动态命名属性,要使用中括号[],而且中括号中的对象会被当作表达式求值

javascript
const name = "lsn";

let person = {};
person[name] = "matt";
person.lsn; //matt

function getName(name) {
  return `${name}`;
}
person = {
  [getName("lsn")]: "matt",
};
person.lsn; //matt
//可计算表达式中抛出任何错误都会中断对象的创建;计算属性的表达式抛出错误,之前完成的计算是不能回滚的
简写方法名
javascript
let person = {
  sayName: function () {
    console.log(name);
  },
};
//简写
let person = {
  sayName(name) {
    console.log(name);
  },
};
person.sayName("lsn"); //lsn

//简写对于访问器属性的获取函数和设置函数也适用
let person = {
  name_: "",
  get name() {
    return this.name_;
  },
  set name(name) {
    this.name_ = name;
  },
  sayName() {
    console.log(name_);
  },
};
person.name; //''
person.name = "matt";
person.name_; //matt

//简写方法名可以与计算属性键相互兼容
const name = "sayName";
let person = {
  [name](name) {
    console.log(name);
  },
};
person.sayName("lsn"); //lsn
对象解构

使用与对象匹配的结构来实现对象属性的赋值

javascript
let person = {
  name: "lsn",
  age: 19,
};
let { name: personName, age: personAge } = person;
personName; //lsn
personAge; //19

//简写
let { name, age } = person;
name; //lsn
age; //19

//如果不能匹配,则该变量的值就是undefined
let { name, job } = person;
name; //lsn
job; //undefined

//可以在结构赋值的同时定义默认值
let { name = "h", job = "h" } = person;
name; //lsn
job; //h

结构函数在内部使用 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象,这意味着在对象解构的上下文中,原始值会被当成对象,所以 null 和 undefined 不能被解构,否则会报错

javascript
let { length } = "foo";
length; //3
let { cunstructor: c } = 4;
c === Number; //true

let { _ } = null; //TypeError
let { _ } = undefined; //TypeError

解构并不要求变量必须在解构表达式中声明,但是如果给事先声明的变量赋值,赋值表达式需要用括号括起来

javascript
let personName, personAge;
let person = {
  name: "lsn",
  age: 19,
}(({ name: personName, age: personAge } = person));

可以利用解构来复制对象属性

javascript
let person = {
  name: "lsn",
  age: 19,
  say: { h: "hello" },
};
let obj = {};
({ name: obj.name, age: obj.age, say: obj.say } = person);
//但是say属于引用,person和obj中的say指向同一个对象,所以改变一个另一个也会变

//套娃
let {
  say: { h },
} = person;
h; //hello
//可以看作

在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样

javascript
let personCopy = {};

//foo在源对象上是undefined
({foo: {title: personCopy.title}} = person);
//报错

//say在目标对象上是udefined
({say: {h: person.say.h}} = person);
//报错

如果解构中出错,则出错前赋值成功,出错后赋值失败,为 undefined

函数参数中也可以使用解构赋值

javascript
function get(name, {hello, f: personf}, age) {
    ...
}

因为在 ECMAScript 中 Object 是派生其他对象的基类,Object 所有属性和方法在派生对象上同样存在

方法
constructor

用于创建当前对象的函数,let obj = new Object();中的Object()函数就是这个属性的值

hasOwnProperty(PropertyName)

用来判断当前对象实例(不是原型)上是否存在给定的属性,参数必须是字符串

isPrototypeof(object)

用来判断当前对象是否为另一个对象的原型

propertyIsEnumerable(propertyName)

用来判断给定的属性是否可以用 for-in 语句枚举

toLocaleString()

返回对象的字符串表示,该字符串反应对象所在的本地化执行环境

toString()

返回对象的字符串表示

valueOf()

返回对象对应的字符串、数值或布尔值表示,通常与 toString()的返回值相同

Object.getOwnPropertySymbols()

获取对象上所有符号属性(es6)

Object.keys(obj)

接受一个对象参数,返回该对象所有可枚举属性名称的字符串数组

Object.getOwnPropertyNames()

返回对象所有属性,不管是不是可枚举属性

以上两者和 for-in 属性在适当的时候可以互相替换

for-in 和 Object.keys()方法枚举顺序是不定的,先以升序枚举数值键,然后以插入顺序枚举字符串和符号键,在对象字面量中定义键以他们逗号分隔的插入顺序枚举

Object.values()

返回对象属性值的数组,执行浅复制,忽略符号属性

Object.entries()

返回对象键值对数组,非字符串属性会被转为字符串输出,执行浅复制,忽略符号属性

冻结对象

将对象冻结,不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值,该对象的原型也不能被修改

虽然修改对象的属性不会报错(在严格模式下会报错),但是操作无效

javascript
let obj = {};
Object.freeze(obj);
obj.name = "lsn";
console.log(obj.name); //undefined

1、属性增加

javascript
mrDeng.wife = "xiaoliu";	//点语法,这种方法只能给属性名字母数字字符
mrDeng["wife"] = "xiaoliu";	//这种方法可以给属性名包含非字母数字字符

//结果
var mrDeng = {
    name : "MrDeng",
    age : 40,
    sex : "male",
    health : 100,
+=> wife : "xiaoliu",
    smoke : function () {
        console.log("I am smoking cool!!!");
        this.health --;
    },
    drink : function () {
        console.log("I am drinking");
        this.health ++;
    }
}

2、属性修改

javascript
mrDeng.sex = "female";	//点语法

//结果
var mrDeng = {
    name : "MrDeng",
    age : 40,
==> sex : "female",
    health : 100,
    wife : "xiaoliu",
    smoke : function () {
        console.log("I am smoking cool!!!");
        this.health --;
    },
    drink : function () {
        console.log("I am drinking");
        this.health ++;
    }
}

3、属性删除

只能使用 delete,返回 boolean 值,如果该属性不存在则返回 undefined,存在则返回 true 并删掉

javascript
delete mrDeng.wife;	//点语法

//结果
var mrDeng = {
    name : "MrDeng",
    age : 40,
    sex : "male",
    health : 100,
-=>
    smoke : function () {
        console.log("I am smoking cool!!!");
        this.health --;
    },
    drink : function () {
        console.log("I am drinking");
        this.health ++;
    }
}

//var声明的全局变量、在函数范围内声明的局部变量所增加的属性一定是不可配置的属性,不能进行delete操作

4、属性查询

javascript
mrDeng.sex; //点语法
mrDeng["sex"];

//输出
male;

创建对象-工厂模式

javascript
function creat(a, b) {
  let o = new Object();
  o.a = a;
  o.b = b;
  return o;
}

创建对象-构造函数模式

javascript
//要遵守大驼峰式命名规则,以区分普通函数和构造函数
function Person() {}

//如果不想传参数,构造函数后面的括号可加可不加
var person1 = new Person();
let person1 = new Person();

function Car(color) {
  this.color = color;
  this.name = "BMW";
  this.height = "1400";
  this.lang = "4900";
  this.weight = 1000;
  this.health = 100;
  this.run = function () {
    this.health--;
  };
}

var car = new Car("blue");
构造函数内部原理
javascript
//当一个函数被new的时候,会隐式产生一个新对象,这个新对象上的[[prototype]]特性被赋值为构造函数prototype属性
//构造函数内部的this被赋值为这个新对象(this指向这个新对象)
//然后执行构造函数内部代码,最后隐式的返回这个this指向的对象(如果构造函数有显式地返回对象,则返回函数中显式返回的对象而不是创建的这个新对象)

function Student(name, age, sex){
    //var this = {
    //	name : "",
    //	age : "",
    //	sex : ""
	//}
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.grade = 2017;
    //return this;
}

var stu = new Student(name, age, sex);

//如果显式模拟return返回一个对象则会改变构造函数返回的对象
function Student(name, age, sex){
    //var this = {
    //	name : "",
    //	age : "",
    //	sex : ""
	//}
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.grade = 2017;
    return {};
}

var stu = new Student(name, age, sex);
stu => {}

//但是如果你返回的是原始值,不是对象值的时候,将会忽略return正常返回
function Student(name, age, sex){
    //var this = {
    //	name : "",
    //	age : "",
    //	sex : ""
	//}
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.grade = 2017;
    return 123;
}

var stu = new Student(name, age, sex);
stu => {name : name, age : age, sex : sex};

构造函数构造的对象在控制台输出的名为该对象 constructor 名

javascript
function test() {}
new test();

image-20210529144904777

构造函数里的方法会在每个实例上创建一遍

javascript
function Person() {
    this.say: function() {
        console.log("hello");
    }
    //逻辑等价于 this.say: new Function("console.log('hello')");
}

let a1 = new Person();
let a2 = new Person();
a1.say === a2.say;	//false

//解决办法是可以将函数定义在外面
function say() {
    console.log("hello");
}
function Person() {
    this.say: say;
}

创建对象-原型模式

原型 Prototype

原型是 function 对象的一个属性,它定义了的构造函数制造出的对象的公共祖先(可以理解为父类)。通过该构造函数产生的对象,也可以继承该原型的属性和方法,原型也是对象,并且会初始化一个隐式变量为 constructor,其值为构造函数。

javascript
Person.prototype.name = "hehe";
Person.prototype.say = function () {
    console.log("hh");
}
//或者
//但是下面这种方式会导致原型中没有constructor
Person.prototype = {
    name : "hehe"
    say : function () {
        console.log("hh");
    }
}

function Person () {};
var person = new Person();
console.log(person);

image-20210515154502694

原型增删改查,增加用xxx.prototype.xxx = xxx;,删除用delete xxx.prototype.xxx,查用xxx.xxx就行,修改用xxx.prototype.xxx = xxx;

如果某个属性在当前对象未被定义时,会去原型 prototype 中查找。

除查询外,其他方法用xxx.xxx将会是修改当前实体对象,并不是原型。

constructor 可以手动修改
javascript
function Person() {}
Car.prototype.constructor = Person;
function Car() {}
var car = new Car();

image-20210515161128796

在之前讲过的 new 时三段构造里,this 其实并不是空的,这样做是为了链接当前对象以及该对象的 prototype,并且这种链接方式为引用。所以上方查询的描述才能实现。

javascript
//在new的时候,所进行的三步
function Person() {
    //var this = {
    //		__proto__ : Person.prototype
	//};
    .......//某些赋值语句
    //this = {
    //		......//赋值语句产生的属性 xxx : "xxx"/function ()
    //		__proto__ : Person.prototype
	//}
    //return this;
}

下面代码出现的情况是因为__proto__和 prototype 指向的空间已经不一样了

javascript
Person.prototype.name = "sunny";
function Person() {}
var person = new Person();
Person.prototype.name = "cherry";
//person.__proto__.name = "cherry"

//但是如果换个方式进行
Person.prototype.name = "sunny";
function Person() {}
var person = new Person();
Person.prototype = {
  name: "cherry",
};
//person.__proto__.name = "sunny"

//理解
Person.prototype = { name: "a" };
__proto__ = Person.prototype;
Person.prototype = { name: "b" };

//我们再换一下位置
Person.prototype.name = "sunny";
function Person() {}
Person.prototype = {
  name: "cherry",
};
var person = new Person();
//person.__proto__.name = "cherry"

自己加的__proto__将不会有继承效果,但是会有这个属性,不归于系统所属。

原型知识点补充

默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数

javascript
Person.prototype.constructor === Person; //true

06087DDB8D5BC8FD00BB8EEF9B594614

可以用 isPrototypeOf()方法确定调用它的对象是不是传入参数对象的 prototype(原型链中适用)

Person.prototype.isPrototypeOf(person1); //true

可以用 Object.getPrototypeOf()返回参数内部[[Prototype]]的值

Object.getPrototypeOf(person1) == Person.prototype; //true

Object.setPrototypeOf(),传入两个参数 a 和 b,会将 b 覆盖到 a 的[[Prototype]]属性值上(但是这个方法不建议用,严重影响代码性能

可以通过 Object.create()解决这个问题,详情请看上文创建对象部分

原型的弊端

原型对象会共用同一个引用属性,所以一个实例所做的更改,另一个实例也会相查询到

原型链

可以将原型链比作作用域链

javascript
function SuperType() {
    this.property = true;
}
//对于xxx.f(),f函数中的this为xxx对象
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}

SubType.prototype = new SuperType();

subType.prototype.getSubValue() = function() {
    return this.subproperty;
}
let instance = new SubType();
console.log(instance.getSuperValue());	//true
B21767949106627C57F44A1858BF89EA

所有对象的最终原型是 Object.prototype,Object.prototype 的原型是 null

D1CECF115C21345E0CDDAB926201B808

对象修饰

原始值坚决不能有属性和方法,但是原始值如果以对象方法创建则可以加属性,同时还可以以原始值进行正常原始值的方法

javascript
var num = new Number(123);
num.a = "a";

//console:
// > num
// < Number {123, a: "a"}
// > num*2
// < 246
// > num
// < Number {123, a: "a"}

//Number、String、Boolean可以,但是undefined、null不可以有属性

上面说到原始值不能有属性和方法,但是

javascript
var str = "abcd";
str.abc = "a";

//console
// < str.abc
// > undefined
// < str.length
// > 4
//上面的原始值str可以访问length,但是不能创建abc对象

包装类

toString() 方法的包装类:Object、Number、Array、Boolean、String

javascript
Number.prototype.toString = function () {
  return "hello lsn";
};

//console
// > var num = 123;
// < undefined
// > num.toString();
// < "hello lsn"

document.write(xxx) 其实调用的是 xxx 的 toString 方法

Boolean、Number、String

包装类:系统在你对原始值进行属性创建时会新建一个原始值对象,进行属性添加后立马删除,在你访问一个原始值属性时,同样的也会创建一个原始值对象,但这个原始值对象与前一个并不相同,没有 len 这个属性。当然,这俩对象也不会与原始值相同。

javascript
var num = 4;
num.len = 3;
//new Number(4).len = 3;  =>  delete
console.log(num.len);
//new Number(4).len

//输出 undefined

var str = "abcd";
str.length = 2;
//new String("abcd").length = 2;  =>  delete
console.log(str.length);
//new String("abcd").length
//这是因为string对象里就有自带的length属性

Object 构造函数作为一个工厂方法,会根据传入值的类型返回相应的原始值包装类型的实例

javascript
let obj = new Object("str");
console.log(obj);
console.log(typeof obj); //object
console.log(obj instanceof String); //true

image-20210710114545618

而且要区分转型函数构造函数

javascript
let value = "25";
let num = Number(value); //转型函数
console.log(typeof num); //"number"
let obj = new Number(valule); //构造函数
console.log(typeof obj); //"object"
Boolean

Boolean 类型重写了 valueOf()、toString()

第一个方法返回原始值,后面方法返回字符串值

原始值和实例化对象差异,不建议使用实例化对象

javascript
let value = false;
let obj = new Boolean(false);

let res = obj && true; //true,所有的对象在布尔表达式中都为true,因为Boolean(obj)返回true
res = value && true; //false

console.log(typeof value); //boolean
console.log(typeof obj); //object
console.log(obj instanceof Boolean); //true
console.log(value instanceof Boolean); //false
Number

Number 类型重写了 valueOf()、toString()、toLocaleString()

第一个方法返回原始值,后面方法返回字符串值

toFixed(num):保留 num 位小数(一般支持 0~20),返回字符串

var a = 123.45678; var num = a.toFixed(3); 123.457

toExponential(num):保留 num 位小数,返回科学计数法字符串

var a = 10; var num = a.toExponential(1); 1.0e+1

toPrecision(num):保留 num 位数,根据情况合理调用 toFixed 和 toExponential

var a = 99; var num = a.toPrecision(1); 1e+2 var num = a.toPrecision(2); 99 var num = a.toPrecision(3); 99.0

原始值和实例化对象与 Boolean 有同样的差异,不建议使用实例化对象

isInteger():确定一个数是否为整数
javascript
Number.isInteger(1); //true
Number.isInteger(1.0); //true
Number.isInteger(1.01); //false
isSafeInteger():是否在安全数值内(IEEE 754 整数范围内)

Number.MIN_SAFE_INTEGER (-2^53 + 1) ~ Number.MAX_SAFE_INTEGER (2^53 - 1)

javascript
Number.isSafeInteger(-1 * 2 ** 53); //false
Number.isSafeInteger(-1 * 2 ** 53 + 1); //true
String

字符串由 16 位码元组成(code unit),每位码元对应一位字符;JavaScript 采用 Unicode 混合编码策略:UCS-2 和 UTF-16

三个继承方法 valueOf()、toLocaleString()、toString()方法都返回对象原始字符串值

length 属性

返回字符数量,其实是返回字符串包含多少个 16 位码元

str.toUpperCase():

将字符串转换为大写

str.toLowerCase():

将字符串转换为小写

str.charAt(x):

返回索引位置 x 的字符

str.charCodeAt(x):

查看指定索引位置 x 的码元值

fromCharCode(...code unit):

接收任意数量的码元值创建字符串

javascript
let str = String.fromCharCode(0x61, 0x62, 0x63); //"abc"
str = String.fromCharCode(97, 98, 99); //"abc"

U+0000~U+FFFF 范围内的 65536 个字符,在 Unicode 中被称为基本多语言平面(BMP),为了表示更多字符,Unicode 采用了每个字符使用另外 16 位去选择一个增补平面的策略(也就是这种字符的每个字符采用一对 16 位码元的策略,也被称为代理对

javascript
let message = "ab😊de";
console.log(message.length); //6,因为实质上是返回字符串含有多少个16位码元,所以不是5,二是6,笑脸占两个16位码元
console.log(message.charAt(1)); //b
console.log(message.charAt(2)); //<?>
console.log(message.charAt(3)); //<?>
console.log(message.charAt(4)); //d

console.log(message.charCodeAt(1)); //98
console.log(message.charCodeAt(2)); //55357
console.log(message.charCodeAt(3)); //56842
console.log(message.charCodeAt(4)); //100

console.log(String.fromCodePoint(0x1f60a)); //😊

console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); //ab😊de

fromCharCode 能够正确解析代理对,并正确将其识别为一个 Unicode 字符

codePointAt(x):

接受 16 位码元的索引并返回该索引位置上的码点(code point)

码点是 Unicode 中一个字符的完整标识,如 c:0x0063、😊:0x1F60A;所以说码点可能是 16 位,也可能是 32 位

javascript
console.log(message.codePointAt(1)); //98
console.log(message.codePointAt(2)); //128522
console.log(message.codePointAt(3)); //56842,错误
console.log(message.codePointAt(4)); //100

//如果传入的码元索引并非代理对的开头,就会返回错误码点
//迭代字符串可以正确的识别代理对的码点
console.log([..."ab😊de"]); //["a", "b", "😊", "d", "e"]
fromCodePoint():

接收任意数量的码点创建字符串

javascript
let str = String.fromCodePoint(97, 98, 128552, 100, 101); //ab😊de
normalize(xx):规范化字符为 xx 格式

某些 Unicode 字符有多种编码方式

javascript
let a1 = String.fromCharCode(0x00c5),
  a2 = String.fromCharCode(0x212b),
  a3 = String.fromCharCode(0x0041, 0x030a);
console.log(a1); //Å
console.log(a2); //Å
console.log(a3); //Å

//这三者字符看起来一摸一样,但是比较操作符只看码元是否一样,所以
a1 != a2;
a2 != a3;
a1 != a3;

为解决这个问题,Unicode 提出四种规范化格式:NFD(Normalization From D)、NFC(Normalization From C)、NFKD(Normalization From KD)、NFKC(Normalization From KC)

javascript
//U+00C5是对0+212B进行NFC、NFKC规范化之后的结果
console.log(a1 === a1.normalize("NFD")); //false
console.log(a1 === a1.normalize("NFC")); //true
console.log(a1 === a1.normalize("NFKD")); //false
console.log(a1 === a1.normalize("NFKC")); //true

//U+212B是为规范化的
console.log(a2 === a2.normalize("NFD")); //false
console.log(a2 === a2.normalize("NFC")); //false
console.log(a2 === a2.normalize("NFKD")); //false
console.log(a2 === a2.normalize("NFKC")); //false

//U+0041/U+030A是0+212B进行NFD、NFKD规范化之后的结果
console.log(a3 === a3.normalize("NFD")); //true
console.log(a3 === a3.normalize("NFC")); //false
console.log(a3 === a3.normalize("NFKD")); //true
console.log(a3 === a3.normalize("NFKC")); //false

选择同一种规范化格式能让比较操作符返回正确的结果

javascript
console.log(a1.normalize("NFD") === a2.normalize("NFD")); //true
console.log(a2.normalize("NFKC") === a3.normalize("NFKC")); //true
console.log(a1.normalize("NFC") === a3.normalize("NFC")); //true
concat():

将一个或多个字符串拼接成一个新字符串(不改变原字符串)

javascript
let a = "hello ";
let a = a.concat("world"); //"hello world"

let a = a.concat("world", "!"); //"hello world!"
slice()、substring():

接收一个或两个参数,第一个参数表示起始位置(包括该位置),第二个参数表示结束位置(不包括该位置,如果没有第二个参数则默认第二个参数为字符串长度),不改变原字符串,返回字符串子串

slice()方法会将所有负参数值转为字符串长度加负参值(类似于 python)

substring()方法会将所有的负参数转化为 0

substr():

接收一个或两个参数,第一个参数表示起始位置(包括该位置),第二个参数表示返回字符串的长度(如果没有第二个参数则默认第二个参数为字符串从起始位置开始的剩余长度),不改变原字符串,返回字符串子串

substr()方法会将第一个负参数转换为字符串长度加负参值,第二个负参数会被转换为 0

indexOf()、lastIndexOf():

从字符串中搜索传入的字符串并返回位置(如果搜不到则返回-1),indexOf 从头开始搜,lastIndexOf 从尾部开始搜

接收一个或两个参数,第一个参数表示要寻找的字符串,第二个参数表示从该位置开始搜

javascript
let str = "hello world";
str.indexOf("o"); //4
str.lastIndexOf("o"); //7

str.indexOf("o", 6); //7
str.lastIndexOf("o", 6); //4
startsWith()、includes()、endsWith():

可以传入一个或两个参数,第一个参数为要检查的字符串,第二个参数定义检查的起始位置,返回布尔值

startsWith()方法检测这个字符串是否为被检测字符串的开头(从 0 开始检查),如foobar中,foo 为开头,但是 bar 不是

includes()方法检测整个字符串(从 0 开始检查),只要是被检测字符串的子串就行

endsWith()方法检查字符串是否是被检测字符串的结尾(从结尾开始检查),如果加了第二个参数则默认字符串长度为第二个参数

javascript
let str = "foobarbaz";
console.log(str.endsWith("barb", 6)); //false
console.log(str.endsWith("bar", 6)); //true
console.log(str.endsWith("baz", 6)); //false
trim():

删除字符串开头和结尾的所有空格,不改变原字符串

repeat():

接收一个整数参数,表示将字符串要重复多少次,然后将重复字符串拼接起来,不改变原字符串

padStart()、padEnd():

可以传入一个或两个参数,第一个参数表示复制的副本长度,第二个参数表示原字符串长度不足时的填充字符串,不改变原字符串

如果副本长度小于原字符串则返回原字符串

javascript
let str = "foo";
console.log(str.padStart(6)); //"   foo"
console.log(str.padStart(6, ".")); //"...foo"
console.log(str.padEnd(6, ".")); //"foo..."
console.log(str.padStart(8, "bar")); //"barbafoo"
console.log(str.padStart(2)); //"foo"
迭代与解构
javascript
//字符串原型上暴露了@@iterator方法,可以手动使用迭代器
let str = "abcd";
let iterator = str[Symbol.iterator]();
console.log(iterator.next()); //{value: "a", done: false}
console.log(iterator.next()); //{value: "b", done: false}
console.log(iterator.next()); //{value: "c", done: false}
console.log(iterator.next()); //{value: "d", done: false}
console.log(iterator.next()); //{value: undefined, done: true}

//for-of可以通过这个迭代器访问每个字符
for (const x of "abcd") {
  console.log(x);
}

//有了这个迭代器后我们就可以通过结构操作符解构这个字符串了
console.log([...str]); //["a", "b", "c", "d"]
replace():

接收两个参数,第一个可以是字符串(不会转换为正则表达式)或 RegExp 对象,第二个参数可以是一个函数或一个字符串,将匹配字符替换,不改变原字符串

涉及正则表达式后文有详解

第一个参数为字符串则只会替换第一个子串

javascript
let str = "abc dbc";
str.replace("bc", "ood"); //"aood dbc"
split():

接收一个参数,该参数可以是字符串或 RegExp 对象,将匹配字符作为分隔符分割字符串

涉及正则表达式后文有详解

javascript
let str = "ab,c,d";
str.split(","); //["ab", "c", "d"]
localeCompare():

按照字母表顺序比较两个字符串,字符串应该在字符串参数前则返回-1(通常为-1,具体要看与实际值相关的实现),如果相等则返回 0,如果应该在后则返回 1(通常为 1,具体要看与实际值相关的实现)

javascript
let val = "abc";
val.localeCompare("bcc"); //-1
val.localeCompare("abc"); //0
val.localeCompare("aab"); //1

有一点注意的是该方法会会根据实现所在的地区(国家和语言)决定比较顺序,不同国家不一样,例如在英国,该函数区分大小写,大写字母排在小写字母前,但在其他国家不一定如此

小题目:求字符串字节长度
javascript
var str = "fasdfasfasfhl;;laf";
//当前字符位的unicode > 255,那么该字符串长度为2
// <= 255 为1
function bytesLength(str) {
  let sum = str.length;
  for (let i = 0; i < str.length; i++) {
    if (str.charCodeAt(i) > 255) {
      sum++;
    }
  }
  return sum;
}

单例内置对象

ECMA-262 对内置对象定义是:“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就已经存在的对象”,开发者不用显式实例化内置对象,因为他们已经实例化好了,实例对象有:Object、Array、String、Global、Math 等

Global

Global 是 ECMAScript 中最特别的对象,因为代码不会显式地访问它。ECMA-262 规定 Global 是种兜底对象,她针对的是不属于任何对象的属性和方法。实际上不存在全局变量和全局函数这种东西,全局作用域中定义的变量和函数都会变成 Global 对象的属性,例如 isNaN 等方法都是属于 Global 对象上的,Global 上还有一些其他方法:

encodeURI()、encodeURIComponent():

这两个方法用于编码统一的资源标识符(URI),以便传给浏览器,有效的 URI 不能包含某些字符如空格

encodeURI 对整个 URI 进行编码,而 encodeURIComponent 只对 URI 中单独组件编码,例如www.lsn.com/illegal value.jsillegal value.js

encodeURI 不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问号、井号,而 encodeURIComponent 会编码它发现的所有非标准字符

javascript
let uri = "http://www.lsn.com/illegal value.js#start";
console.log(encodeURI(uri)); //"http://www.lsn.com/illegal%20value.js#start"
console.log(encodeURIComponent(uri)); //"http%3A%2F%2Fwww.lsn.com%2Fillegal%20value.js%23start"

所以我们一般用 encodeURI 编码整个 uri,然后用 encodeURIComponent 编码要追加到已有 uri 后面的字符串

decodeURI()、decodeURIComponent():

decodeURI 解码所有被 decodeURI 编码过的字符,而 decodeURIComponent 解码所有被 encodeURIComponent 编码过的字符

javascript
let uri = "http%3A%2F%2Fwww.lsn.com%2Fillegal%20value.js%23start";
console.log(decodeURI(uri)); //"http%3A%2F%2Fwww.lsn.com%2Fillegal value.js%23start"
console.log(decodeURIComponent(uri)); //"http://www.lsn.com/illegal value.js#start"
eval():

整个 ECMAScript 中最强大的方法,它是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(Javascript)字符串,然后将其解释为实际的 ECMAScript 语句,然后将其插入该位置

eval 执行时相当于把参数字符串拿出来放在 eval 所处的位置,eval 中定义的变量和函数都能够在其后访问调用,但是不能在其前面调用,因为 eval 还没执行,参数就只是字符串。

但是在严格模式下,eval 内部创建的变量和函数无法被外部访问,同样赋值给 eval 也会报错

Global 属性

undefined、NaN、Infinity、Object、Array、Function、Boolean、String、Number、Date、RegExp、Symbol、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError

window

虽然 ECMAScript 没有直接规定 Global 对象的访问方式,但是浏览器将 window 对象实现为 Global 对象的代理。因此,全局作用域中声明的对象和函数都变成了 window 的属性

另一种获取 global 对象的方式:当一个函数没有明确(通过成为某个对象的方法或者通过 call()/apply())指定 this 的情况下执行时,this 值等于 global 对象

javascript
let global = (function () {
  return this;
})();
Math

math 对象属性:

E(自然对数的基数 e 的值)、LN10(10 为底的自然数对数)、LN2、LOG2E(以 2 为底的 e 的对数)、LOG10E、PI(Π 的值)、SQRT1_2(1/2 的平方根)、SQRT2(2 的平方根)

math 方法:

min()、max():

传入多个参数,返回最小值或最大值

ceil():

向上舍入为最接近的整数

floor():

向下舍入为最接近的整数

round():

执行四舍五入

fround():

返回数值最接近的单精度(32 位)浮点值表示

random():

返回一个 0~1 范围内的随机数,包含 0 但不包含 1

自定义随机数函数
javascript
function selectFrom(x, y) {
  let range = y - x + 1;
  return Math.floor(Math.random() * range + x);
}
selectForm(2, 10); //选择一个2-10的整数,包含2和10

还有很多方法,在这里就不多描述了,请查阅红宝书(p134)或上网寻找

JavaScript 只处理 16 位有效数(小数点前后 16 位),不然会直接截断

image-20210517143037892

image-20210517143421671

call/apply

apply 与 call 基本一样,俩者传参列表不同,apply 只能传一个形参数组

javascript
call(obj, a, b);  -->  一个个传形参
apply(obj, [a, b]);  -->  直接传arguments

改变 this 指向

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

var person = new Person("deng", 100);
var obj = {};
Person.call(obj, "deng", 100); //Person里的this将会改变指向为obj,call函数传参顺序是this、原函数形参
//这样后,new的三段式变为
//this = obj;
//this.name = name;
//this.age = age;
//return this;
//obj = {name:name, age:age};

//任何函数都有call方法,我们在调用函数时其实是调用call();   test();  -->  test.call();
//如果call(obj)中obj为null或者undefined的话,this的默认值还是window

如果 call 方法不传原函数形参

image-20210517144853461

如果传参

image-20210517144923579

应用

javascript
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}

function Student(name, age, sex, tel, grade) {
  Person.call(this, name, age, sex);
  this.tel = tel;
  this.grade = grade;
}

var student = new Student("sunny", 123, "male", 139, 2017);

继承

1、传统模式——原型链

过多的继承了没用的属性

2、借用构造函数(call、apply)

不能继承借用构造函数的原型

每次构造函数都要多走一个函数

3、共享原型

不能随意改动自己的原型,因为 Son 和 Father 的原型已经指向同一 prototype

javascript
Father.prototype.lastName = "Deng";
function Father() {}

function Son() {}

//1.Son.prototype = Father.prototype

//2.抽象出继承功能
function inherit(Target, Origin) {
  Target.prototype = Origin.prototype;
}
inherit(Son, Father);
var son = new Son();

//								Father.prototype
//				Father									Son

4、圣杯模式(寄生式组合继承,特别推荐)

通过一个中间函数来解决方法 3 中的问题,使得 Father 和 Son 的 prototype 不为直接引用,而是创建

javascript
Father.prototype.lastName = "Deng";
function Father() {

}

function Son() {

}

function inherit(Target, Origin){
    function F() {};
    F.prototype = Origin.prototype;
    Target.prototype = new F();
}
inherit(Son, Father);
var son = new Son();

son.__proto__  -->  new F()  -->  F.__proto__  -->  Father.prototype

image-20210518093640053

我们需要在 inherit 函数里优化这个问题

javascript
function inherit(Target, Origin) {
  function F() {}
  F.prototype = Origin.prototype;
  Target.prototype = new F();
  Target.prototype.constructor = Target;
  Target.prototype.uber = Origin.prototype; //真正的继承来自于
}

//在yahoo和YUI3库里是这么定义的
var inherit = (function () {
  var F = function () {};
  return function (Target, Origin) {
    F.prototype = Origin.prototype;
    Target.prototype = new F();
    Target.prototype.constructor = Target;
    Target.prototype.uber = Origin.prototype;
  };
})();
//这样子后F函数将会成为一个私有变量

//优化代码
function inherit(Target, Origin) {
  let F = Object.create(Origin.prototype);
  Target.prototype = new F();
  Target.prototype.constructor = Target;
}

image-20210519140547644

例子
javascript
function fa(name) {
  this.name = name;
}
function so(name, age) {
  fa.call(this, name);
  this.age = age;
}

inherit(so, fa);
let ins = new so();

5、组合继承

实例不会共享原型非公共的引用值,但是可以共享公共的值(一般推荐

javascript
function fa(name) {
  this.name = name;
}
function so(name, age) {
  fa.call(this, name);
  this.age = age;
}

so.prototype = new fa();
let ins = new so("lsn", 19);

但是也会有弊端:第一是父类构造函数调用两次,第二是子类实例上有一组父类构造函数构造出来的属性,然而子类原型上也有

命名空间

为了防止团队开发时命名重复问题,老办法采取措施

javascript
var org = {
	a : {
		ff : 123;
	},
	b : {
		ff : function () {}
	}
}

也可以采用闭包

javascript
var init = (function () {
  var name = "abc";

  function callName() {
    console.log("name");
  }
  return function () {
    callName();
  };
})();

init();

模拟 jq 的连续执函数

javascript
var deng = {
  a: function () {
    console.log("a");
    return this;
  },
  b: function () {
    console.log("b");
    return this;
  },
  c: function () {
    console.log("c");
    return this;
  },
};
deng.a().b().b().c();

对 obj 对象调用时,其实真正调用的是后者

javascript
obj.name-- > obj["name"];

对象枚举

遍历 枚举 enumeration

对象

javascript
var obj = {
  name: "name",
  sex: "sex",
  height: "height",
  __proto__: {
    hh: "hh",
  },
};

for in

javascript
for (var prop in obj) {
  console.log(prop, typeof prop);
}
//key in obj
//prop为对象属性名,会包括prototype
//之后调用可以采用obj[prop]
//不能采用obj.prop方式,因为obj会认为'.'后是属性字符串名,所以会转成obj['prop']

image-20210520095624614

1、因为直接用 for-in 会将 prototype 也遍历到,所以我们用 hasOwnProperty 方法
javascript
for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(obj[prop]);
  }
}
//hasOwnProperty接触到含有__proto__为Object的原型链时会返回false
//虽然我自己定义了一个—__proto__,但是这个__proto__里的__proto__是Object
obj = {
  __proto__: {
    hh: "hh",
    __proto__: Object,
  },
};

image-20210520095538051

javascript
Object.prototype.abc = "deng";
for (var prop in obj) {
  if (!obj.hasOwnProperty(prop)) {
    console.log(obj[prop]);
  }
}

image-20210520190927973

2、in

判断属性是否在对象里(包括原型),in 前要加属性名,而且要是字符串,不然系统会认为是变量,返回 Boolean 型

javascript
> 'height' in obj
< true
> 'hh' in obj    -->    hh在__proto__里
< true

this

image-20210520232049424

对于一个函数的调用,谁调用了这个函数,这个函数里的 this 就指向谁

javascript
var name = "222";
var a = {
    name : "111",
    say : function () {
        console.log(this.name);
    }
}

var b = {
    name : "333",
    say : function (fun) {
        fun();
    }
}

b.say(a.say); --> 222

因为 b 种 fun 函数并没有谁调用,所以走预编译过程,this 指向 window

克隆

shallow clone:引用值会跟着某一个的改变而全部改变

javascript
function clone(Origin, Target) {
  var Target = Target || {};
  for (let prop in Origin) {
    Target[prop] = Origin[prop];
  }
}

deep clone:引用值不会随着某一个对象的改变而改变,完全独立但一样

判断是否为原始值:typeof()

判断是否数组或对象:instanceof、toString、constructor

instanceof 和 constructor 在父子域中跨域会返回 false,父子域:例如用 iframe 引进新的子页面,俩个页面之间的参数将会跨域

javascript
function clone(Origin, Target) {
  var Target = Target || {},
    toStr = Object.prototype.toString,
    arrStr = "[object Array]";
  for (let prop in Origin) {
    if (Origin.hasOwnProperty(prop)) {
      if (Origin[prop] !== "null" && typeof Origin[prop] != "object") {
        Target[prop] = Origin[prop];
      } else {
        if (toStr.call(Origin[prop]) == arrStr) {
          Target[prop] = [];
        } else {
          Target[prop] = {};
        }
        clone(Origin[prop], Target[prop]);
      }
    }
  }
  return Target;
}

//运用下方三目简化代码后

function clone(Origin, Target) {
  var Target = Target || {},
    toStr = Object.prototype.toString,
    arrStr = "[object Array]";
  for (let prop in Origin) {
    if (Origin.hasOwnProperty(prop)) {
      if (Origin[prop] !== "null" && typeof Origin[prop] != "object") {
        Target[prop] = Origin[prop];
      } else {
        Target[prop] = toStr.call(Origin[prop]) == arrStr ? [] : {};
        clone(Origin[prop], Target[prop]);
      }
    }
  }
  return Target;
}

代理与反射

代理是目标对象的抽象,类似于 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象可以通过代理操作也可以直接操作,但是直接操作会越过代理赋予的行为

创建空代理

通过 Proxy 构造函数创建,接收两个参数:目标对象和处理程序对象(缺一不可

javascript
let target = {
  id: "target",
};
const handler = {};
const proxy = new Proxy(target, handler);

target.id; //target
proxy.id; //target

target.id = "foo";
target.id; //foo
proxy.id; //foo

//这个赋值会转移到目标对象
proxy.id = "bar";
target.id; //bar
proxy.id; //bar

//hasOwnProperty()在两个地方都会转移到目标对象
target.hasOwnProperty("id"); //true
proxy.hasOwnProperty("id"); //true

//Proxy.prototype是undefined,所以不能使用instanceof

//严格相等可以用来区分代理和目标
target === proxy; //false

定义捕获器

捕获器会在某些操作之前被调用并产生相应行为后继续返回这些操作,可以在处理程序对象中定义多个捕获器

捕获器其实是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流

javascript
const target = {
  id: "lsn",
};
const handler = {
  //捕获器在程序中以方法名为键
  //很多操作都会触发这个get捕获器
  get() {
    return "handler override";
  },
};

let proxy = new Proxy(target, handler);
target.id; //lsn
proxy.id; //handler override

target["id"]; //lsn
proxy["id"]; //handler override

Object.create(target)["id"]; //lsn
Object.create(proxy)["id"]; //handler override

捕获器参数和反射 API

javascript
//get捕获器接收三个参数:目标对象,查询的属性,代理对象
const handler = {
  get(trapTarget, property, receiver) {
    //trapTarget === target
    //property : foo
    //receiver === proxy
  },
};
proxy.foo;

//通过手写代码如法炮制是不现实的,所以可以通过Reflect对象(封装了原始行为)的同名方法来轻松重建
//处理程序对象中所有可以捕获的方法都有对应的反射API
const handler = {
  get() {
    return Reflect.get(...arguments);
  },
};
const handler = {
  get: Reflect.get,
};

//如果真想创建可以捕获所有的方法
let proxy = new Proxy(target, Reflect);

//开发者可以在反射API上修改自己的方法
const handler = {
  get(trapTarget, property, receiver) {
    let r = "";
    if (property == "foo") {
      r = "!!!";
    }

    return Reflect.get(...arguments) + r;
  },
};
target.foo; //bar
proxy.foo; //bar!!!

捕获器不变式

使用捕获器几乎可以改变所有基本方法行为,但也不是没有限制

每个捕获器方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵守“捕获器不变式”

捕获器不变式因方法不同而异,但通常会防止捕获器定义出现过于反常的行为

例如,目标对象有一个不可配置且不可写的数据属性,捕获器返回与该属性不同的值时会抛出异常

javascript
const target = {};
Object.defineProperty(target, "foo", {
  configurable: false,
  writable: false,
  value: "bar",
});
const handler = {
  get() {
    return "bar";
  },
};
let proxy = new Proxy(target, handler);
proxy.foo; //TypeError

可撤销代理

可以调用 Proxy.revocable()方法获取到 revoke()函数,而且撤销函数 revoke()是幂等的,无论调用多少次结果都一样,撤销代理后在调用代理就会抛出 TypeError 异常

javascript
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.foo; //bar
revoke();
proxy, foo; //TypeError

实用

反射 API 并不限于捕获程序

大多数反射 API 方法在 Object 类型上都有对应的方法

很多反射方法都返回”状态标记“的布尔值,表示该方法运行成功与否,这有时候比对象 API 返回修改后的对象或直接抛出异常要更有用

javascript
const o = {};
if (Reflect.defineProperty(o, "foo", { value: "bar" })) {
  console.log("success");
} else {
  console.log("failure");
}

以下反射方法会返回状态标记:

Reflect.defineProperty()

Reflect.preventExtensions()

Reflect.setPrototypeOf()

Reflect.set()

Reflect.deleteProperty()

以下方法可以替代操作符完成的工作:

Reflect.get() 对象属性访问操作符

Reflect.set() =赋值操作符

Reflect.has() 代替 in 操作符或 with()

Reflect.deleteproperty() 代替 delete 操作符

Reflect.construct() 代替 new 操作符

安全地应用函数

有时候我们使用 apply 方法调用函数时,可能函数也有自己的 apply 方法(可能性很小),但是防止这个问题我们可以用 Function 原型上的 apply 方法Function.prototype.apply.call(myFunc, thisVal, argumentsList);

我们可以使用 Reflect.apply 来避免这种可怕的方法(虽然我不知道上面方法哪里可怕,红宝书这么些,先记着)Reflect.apply(myFunc, thisVal, argumentsList);

代理一个代理

可以通过代理一个代理来构建一个多层拦截网

javascript
const target = { foo: "bar" };
const fproxy = new Proxy(target, {
  get() {
    console.log("f");
    return Reflect.get(...arguments);
  },
});
const sproxy = new Proxy(fproxy, {
  get() {
    console.log("s");
    return Reflect.get(...arguments);
  },
});
sproxy.foo;
//s
//f
//bar

代理的问题

1、代理中的 this,因为函数方法中的 this 指向调用函数的对象

javascript
let Foo = {
  say() {
    return this === proxy;
  },
};
let proxy = new Proxy(Foo, {});
Foo.say(); //false
proxy.say(); //true

但是如果遇到依赖实例 this 的对象时,就会出问题,比如 WeakMap,这时候就要把 this 对象在实例之前就指向 proxy

2、代理与内部槽位

有些内置类型不能很好的运用代理,比如 Date:Date 类型方法执行依赖 this 值上的内部槽位[[NumberDate]],而代理对象上不存在这个槽位,这个槽位又不能通过普通的 get()和 set()操作访问到,所以代理拦截后本应转发给目标对象的方法会抛出错误 TypeError

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作

get() - Reflect.get()

返回值:无限值

拦截的操作:proxy.property、proxy[property]、Object.create(proxy)[property]、Reflect.get(proxy, property, receiver)

参数:target、property、receiver(代理对象或继承代理对象的对象)

捕获器不变式:target.property 不可写不可配置,处理程序返回值必须与 target.property 匹配;target.property 不可配置且[[Get]]特性为 undefined,处理程序返回值必须为 undefined

set() - Reflect.set()

返回值:true 表示成功,false 表示失败(严格模式下会抛出 TypeError)

拦截的操作:proxy.prototype = value、proxy[property] = value、Object.create(proxy)[property] = value、Reflect.set(proxy, property, value, receiver)

参数:target、property、value、receiver(接收最初赋值的对象,如 proxy)

捕获器不变式:target.property 不可写不可配置,不能修改目标属性的值;target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性值;严格模式下,处理程序中返回 false 会抛出 TypeError

has() - Reflect.has()

返回值:必须是布尔值,表示属性是否存在,非布尔值会被转为布尔值

拦截的操作:property in proxy、property in Object.create(proxy)、with(proxy) {(property);}、Reflect.has(proxy, property)

参数:target、property

捕获器不变式:target.property 存在且不可配置,则处理程序必须返回 true;target.property 存在且目标对象不可扩展,则处理程序必须返回 true

defineProperty() - Reflect.defineProperty()

返回值:必须返回布尔值,表示属性是否定义成功,非布尔值会被转换成布尔值

拦截的操作:Object.defineProperty(proxy, property, descriptor)、Reflect.defineProperty(proxy, property, descriptor)

参数:target、property、descriptor

捕获器不变式:目标对象不可扩展,则无法定义;目标对象有一个可配置属性,则不能添加同名的不可配置属性;目标对象有一个不可配置属性,则不能添加同名的可配置属性

getOwnPropertyDescriptor() - Reflect.getOwnPropertyDescriptor()

返回值:必须返回对象,或者在属性不存在时返回 undefined

拦截的操作:Object.getOwnPropertyDescriptor(proxy, property)、Reflect.getOwnPropertyDescriptor(proxy, property)

参数:target、property

捕获器不变式:自有的 target.property 存在且不可配置,则处理程序必须返回一个表示该属性存在的对象;自有的 target.property 存在且可配置,则处理程序必须返回表示该属性可配置的对象;自有的 target.property 存在且 target 不可扩展,则处理程序必须必须返回一个表示该属性存在的对象;target.property 不存在且 target 不可扩展,则处处理程序必须返回 undefined 表示该属性不存在;target.property 不存在,则处理程序不能返回表示该属性的可配置对象

deleteProperty() - Reflect.deleteProperty()

返回值:必须返回布尔值,表示属性是否删除成功,非布尔值会被转换为布尔值

拦截的操作:delete proxy.property、delete proxy[property]、Reflect.deleteProperty(proxy, property)

参数:target、property

捕获器不变式:自有的 target.property 存在且不可配置,则处理程序不能删除这个属性

ownKeys() - Reflect.ownKeys()

返回值:必须返回包含字符串或符号的可枚举对象

拦截的操作:Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、Reflect.ownKeys(proxy)

参数:target

捕获器不变式:返回的可枚举对象必须包含 target 的所有不可配置的自有属性;target 不可扩展,则返回可枚举对象必须精准的包含自有属性键

getPrototypeOf() - Reflect.getPrototypeOf()

返回值:必须为对象或者返回 null

拦截的操作:Object.getPrototypeOf(proxy)、Reflect.getPrototypeOf(proxy)、proxy.__proto__、Object.prototype.isPrototypeOf(proxy)、proxy instanceof Object

参数:target

捕获器不变式:target 不可扩展,则 Object.getPrototypeOf(proxy)唯一有效的返回值就是 Object.getPrototypeOf(target)的返回值

setPrototypeOf() - Reflect.setPrototypeOf()

返回值:必须返回布尔值,表示原型是否赋值成功,非布尔值会被转换为布尔值

拦截的操作:Object.setPrototypeOf(proxy)、Reflect.setPrototypeOf(proxy)

参数:target、prototype(target 的替代原型,如果是顶级原型则为 null)

捕获器不变式:target 不可扩展,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target)的返回值

isExtensible() - Reflect.isExtensible()

返回值:必须返回布尔值,表示 target 是否可以扩展,非布尔值会被转化为布尔值

拦截的操作:Object.isExtensible(proxy)、Reflect.isExtensible(proxy)

参数:target

捕获器不变式:target 可扩展,则必须返回 true;target 不可扩展,则必须返回 false

preventExtensions() - Reflect.preventExtensions()

返回值:必须返回布尔值,表示 target 是否已经不可扩展,非布尔值会被转换为布尔值

拦截的操作:Object.preventExtensions(proxy)、Reflect.preventExtensions(proxy)

参数:target

捕获器不变式:target.isExtensible(proxy)返回的时 false,则处理程序必须返回 true

apply() - Reflect.apply()

返回值:无返回值

拦截的操作:proxy(...argumentsList)、Function.prototype.apply(thisArg, argumentsList)、Function.prototype.call(thisArg, ...argumentsList)、Reflect.apply(target, thisArgument, argumentsList)

参数:target、thisArg(this 参数)、argumentsList(调用函数时的参数列表)

捕获器不变式:target 必须是一个函数对象

construct() - Reflect.construct()

返回值:必须返回一个对象

拦截的操作:new Proxy(...argumentsList)、Reflect.construct(target, argumentsList, newTarget)

参数:target、argumentsList、newTarget(最初被调用的构造函数)

捕获器不变式:target 必须可以用作构造函数

代理模式

详情请参阅红宝书 p283-p286

跟踪属性访问

通过捕获 get、set、has 等操作,可以知道对象什么时候被访问、被查询。将实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过

隐藏属性

代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举

属性验证

因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值来决定允许还是拒绝

函数与构造函数参数验证

与保护和验证对象属性类似,也可对函数和构造函数参数进行审查,比如可以让函数只接受某种类型的值

数据绑定与可观察对象

通过代理可以把运行时中原本不相关的部分联系到一起,这样可以实现各种模式,从而让不同的代码互相操作

虽然表面上看起来可以支持正式的面向对象编程,但是实际上它背后使用的任然是原型和构造函数的概念

定义类

javascript
//类声明
class Foo {}
//类表达式
const Foo = class {};

函数声明可以提升,但类表达式不能,类受块作用域限制

构成

构造函数方法、实例方法、获取函数方法、设置函数方法、静态类方法

constructor() {}get f() {}static nn() {}

类表达式的名称是可选的,但是不能被类表达式作用域外部访问

javascript
let p = class Person {
  some() {}
};

let f = new p();

console.log(f); //Person{}
console.log(p); //calss Person{some() {}}
cossole.log(Person); //ReferenceError

类构造函数

与构造函数基本一致

javascript
//当一个类被new的时候,会在内存隐式产生一个新对象,这个新对象上的[[prototype]]特性被赋值为构造函数prototype属性
//构造函数内部的this被赋值为这个新对象(this指向这个新对象)
//然后执行构造函数内部代码,最后隐式的返回这个this指向的对象(如果构造函数有显式地返回对象,则返回函数中显式返回的对象而不是创建的这个新对象)
class Person {
  constructor(color) {
    this.color = color;
  }
}
let p = new Person("green"); //{color: 'green'}

class Person {
  constructor(color) {
    this.color = color;
    return {
      foo: "foo",
    };
  }
}
let p = new Person("green"); //{foo: 'foo'}
//注意:上面这种方法创建的对象和Person没有关联了,因为返回的不是this对象,并且没有指定prototype,也就没有和Person类有关联

如果不需要传参数,则直接 new Person;就行,不用加括号;但是调用类构造函数必须使用 new

类构造函数实例后会变成普通的实例方法,但是作为类构造函数还是必须用 new

javascript
class Person {}
let p = new Person();
console.log(p.constructor); //class Person {}
let p1 = new p.constructor();
console.log(p1); //Person{}

类其实就是一个特殊的函数

javascript
class Person {}
console.log(typeof Person); //function

类标签符有 prototype 属性,这个原型也有一个 constructor 指向自身

javascript
class Person {}
console.log(Person.prototype); //{constructor: f()}
console.log(Person === Person.prototype.constructor); //true

类中定义的 constructor 不会被当成构造函数,但是直接将构造函数当普通构造函数构造的对象却不会

javascript
class Person {}

let p = new Person();
p.constructor === Person; //true
p instanceof Person; //true
p instanceof Person.constructor; //false

let p1 = new Person.constructor();
p1.constructor === Person; //false
p1 instanceof Person; //false
p1 instanceof Person.constructor; //true

类可以像函数一样在任何地方定义,比如数组中,也可以像函数一样作为参数传参,也可以立即实例化

javascript
let arr = [class Person {}];
function f(fun) {
  return new fun();
}
f(arr[0]);

let p = new (class Person {
  constructor(name) {}
})("lsn");

实例、原型和类成员

每个构造函数中的属性在实例上都是单独的,不会在原型上共享,但是在类块中定义的所有内容都会定义在类的原型上

javascript
class Person {
  constructor() {
    this.say = () => {
      console.log("hi");
    };
    this.name = "lsn";
  }
  say() {
    console.log("lll");
  }
}
let p = new Person();
let p1 = new Person();
p.name; //'lsn'
p1.name; //'lsn'
p.name = "l";
p1.name; //'lsn'

p.say(); //hi
p.prototype.say(); //lll

类方法可以使用字符串、符号或计算的值作为键值

javascript
let symbolkey = new Symbol("symbolkey");
class Person {
  say() {}
  [symbolkey]() {}
  ["a" + 1]() {}
}

类定义也支持访问器属性,语法和普通函数一样

javascript
class Person {
  get name() {
    return this.name_;
  }
  set name(name) {
    this.name_ = name;
  }
}
let p = new Person();
p.name = "lsn";
p.name; //'lsn'

静态类

使用 static 作为关键字前缀,在静态成员中,this 引用类自身,其他所有约定和原型一样

javascript
class Person {
    static say() {
        console.log('say');
    }

    static eat() {
        console.log('eat');
    }
}
console.log(Person.say());	//say
console.log(Person.eat());	//eat

//它还可以作为实例工厂
class Person {
    static create() {
        return new Person(..);
    }
}

类可以在定义外向类或类的原型添加成员数据

javascript
class Person {
  sayName() {
    console.log(`${Person.greeting} ${this.name}`);
  }
}

Person.greeting = "lsn";
Person.prototype.name = "handsome";

let p = new Person();
p.sayName(); //lsn handsome

而且这个方法里 this 就是实例本身

javascript
class Person {
  sayName() {
    console.log(`${Person.greeting} ${this.name}`);
    return this;
  }
}

Person.greeting = "lsn";
Person.prototype.name = "handsome";

let p = new Person();
let temp = p.sayName(); //lsn handsome
console.log(p);
console.log(temp === p); //true

迭代器与生成器方法

javascript
class Person {
    //在原型上定义生成器方法
    *createIterator() {
        yield 'lsn';
    }

    //在类上定义生成器方法
    static *createIterator() {
        yield 'lsn';
    }
}

//可以添加一个默认迭代器,将实例变成可迭代对象
class Person {
    constructor() {
        this.name = ['lsn', 'wps', 'lrj', 'nms', 'mzd'];
    }

    *[Symbol.iterator]() {
        yield *this.name.entries();
    }
}

//也可以返回迭代器实例
class Person {
    constructor() {
        this.name = ['lsn', 'wps', 'lrj', 'nms', 'mzd'];
    }

    [Symbol.iterator]() {
        yield this.name.entries();
    }
}

继承

虽然继承是新语法,但是背后依旧使用的是原型链

使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象,这就使得 class 不仅可以继承一个类,也可以继承普通的构造函数

类和原型上定义的方法都会带到派生类

javascript
class Foo {
  say(id) {
    console.log(id, this);
  }

  static say(id) {
    console.log(id, this);
  }
}
class Bar extends Foo {}

let f = new Foo();
let b = new Bar();

f.say("foo"); //foo, Foo {}
b.say("bar"); //bar, Bar {}

Foo.say("foo"); //foo, class Foo {say(id) {console.log(id, this);} static say(id) {console.log(id, this);}}
Bar.say("bar"); //bar class Bar extends Foo {}

super

派生类可以通过 super 关键字引用他们的原型,这个关键字只能在派生类中使用,而且仅限于构造函数、实例方法和静态方法内部

在构造函数中使用 super 可以调用父类构造函数

javascript
class Foo {
  constructor() {
    this.has = true;
  }
}
class Bar extends Foo {
  constructor() {
    //不要在super()前引用this,否则会抛出ReferenceError
    super();

    console.log(this instanceof Foo); //true
    console.log(this); //Bar {has: true}
  }
}
let b = new Bar();

静态方法中可以通过 super 调用继承的类上的静态方法,也只能调用静态方法,而普通类方法中 super 也只能调用父级普通类方法

javascript
class Foo {
  static say() {
    console.log("lsn");
  }

  sayName() {
    console.log("name");
  }
}
class Bar {
  static say() {
    super.say();
  }

  sayName() {
    super.sayName();
  }
}

Bar.say(); //lsn
new Bar().sayName(); //name

es6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象,这个指针是自动赋值的,super 始终会定义为[[HomeObject]]的原型

注意:

super 只能在派生类中使用

不能单独引用 super,必须用它调用构造函数或者引用方法

调用 this 会调用父类构造函数并将返回值传给 this

如果父类构造函数要传参数,则需要手动 super(..., ...)传参

如果没有定义类构造函数,实例化派生类时会调用 super(),而且会传入所有传给派生类的参数如果在派生类中显式定义了一个构造函数,要么必须在其中调用 this,要么返回一个对象

抽象基类

new.target 保存通过 new 关键字调用的类或函数,通过实例化时检测 new.target 是不是抽象基类,可以阻止抽象基类实例化

javascript
class Foo {
  constructor() {
    if (new.target === Foo) {
      throw new Error("error");
    }
  }
}
class Bar extends Foo {}

let b = new Bar();
let f = new Foo(); //Error:error

可以在抽象基类构造函数中进行检查,可以要求派生类必须定义某些方法。因为原型方法在调用类构造函数之前就已经存在,所以可以通过 this 关键字来检查响应方法

javascript
class Foo {
  constructor() {
    if (!this.foo) {
      throw new Error("error");
    }
    console.log("success");
  }
}
class Bar extends Foo {
  foo() {}
}
class Van extends Foo {}

new Bar(); //success
new Van(); //Error:error

继承内置类型

例如继承 Array,定义自己的某些方法,但是产生的实例还是一个数组,可以调用 Array 的所有方法

有些内置类型的方法会返回新的实例,默认情况返回的新实例类型和原始实例类型是一样的,可以用覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类

javascript
class superArr extends Array {}
let a = new superArr(1, 2, 3);
let b = a.filter((x) => !!(x % 2));

a instanceof superArr; //true
b instanceof superArr; //true

class superArr extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}
let a = new superArr(1, 2, 3);
let b = a.filter((x) => !!(x % 2));

a instanceof superArr; //true
b instanceof superArr; //false

混入模式(很多 js 框架,特别是 React 抛弃了混入模式)

extends 后面可以接一个表达式,表达式求值后必须为一个类或者构造函数

javascript
class Foo {
  foo() {
    console.log("foo");
  }
}

let BazMix = (superClass) =>
  class extends superClass {
    baz() {
      console.log("baz");
    }
  };
let BarMix = (superClass) =>
  class extends superClass {
    bar() {
      console.log("bar");
    }
  };

class Bar extends BarMix(BazMix(Foo)) {}

let b = new Bar();
b.bar(); //bar
b.baz(); //baz
b.foo(); //foo

//优化嵌套语句
function mix(baseClass, ...mixins) {
  return mixins.reduce((pre, cur) => cur(pre), baseClass);
}
class Bar extends mix(Foo, BarMix, BazMix) {}

集合引用类型 Object、Array

数组

数组的创建

javascript
var arr = [];
var arr = new Array();

//所有数组方法都来源于Array.prototype

//字面量,不会调用Array构造函数
//数组中空位为undefined,但是es6之前的方法(map()、join等)会忽略空位或者视空位为空字符串,因为这些方法差异,所以要避免使用数组空位,如需要则用显式undefined值代替
var arr = [1,,,,2];
// > arr
// < [1, empty × 3, 2]
// > arr[1]
// < undefined

//构造方法(省略new也是一样的)
var arr = new Array(1,2,3,4,5);  =>  [1,2,3,4,5]
var arr = new Array(10);  => [empty × 10]
var arr = new Array(10.2)  =>  Uncaught RangeError: Invalid array length

数组除了引用它没有的方法,一般不会报错,例如

javascript
var arr = [];

// > arr[10]
// < undefined

// > arr[10] = 10;
// arr = [empty × 10, 10]
ES6 引入两个新的创建数组静态方法:from()、of()

from():用于将类数组结构转换为数组实例

第一个参数接收一个类数组对象(任何可迭代的结构 或者 有一个 length 属性和可索引元素的结构)

javascript
//字符串会被拆分成字符串数组
Array.from("abcd");	//["a", "b", "c", "d"]

//可以将集合和映射转换为一个新数组
const m = new Map().set(1, 2)
				   .set(3, 4);
const s = new Set().add(1)
				   .add(2)
				   .add(3);
Array.from(m);	//[[1, 2], [3, 4]]
Array.from(s);	//[1, 2, 3]

//对现有数组执行深拷贝
const a1 = [1, 2, 3];
const a2 = Array.from(a1);
a1 === a2;	//false

//可以使用任何可迭代对象
const iter = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}
Array.from(iter);	//[1, 2, 3]

//arguments对象可以被轻松的转换为数组
function getArgsArray() {
    return Array.from(arguments);
}
getArgsArray(1, 2, 3, 4);	//[1, 2, 3, 4]

//from也能转换带有必要属性的自定义对象
const arraylikeObject = {
    0: 1,
    1: 2,
    2: 3,
    3: 4,
    length: 4
};
Array.from(arrayLikeObject);	//[1, 2, 3, 4]

from还可以接收第二个可选的映射函数参数,可以直接增强新数组的值,还可以接收第三个课选参数,用来指定第二个函数中this的值(但这个参数在箭头函数中不适用)

const a1 = [1, 2, 3];
const a2 = Array.from(a1, x => x**2);	//[1, 4, 9]
const a3 = Array.from(a1, function(x) {return x**this.exponent}, {exponent: 2});	//[1, 4, 9]

of():用于将一组参数转换为数组,用来替换 es6 之前常用的 Array.prototype.slice.call(arguments)

javascript
Array.of(1, 2, 3, 4); //[1, 2, 3, 4]
Array.of(undefined); //[undefined]
通过 length 改变数组

如果数组长度为 3,改变 length<3 则会相应的删除数组末尾的值,改变 length>3 则会相应补充 undefined 空位

数组的检测

instanceof:在只有一个全局作用域的情况下

Array.isArray(value):在网页里有多个框架,并且框架中含有不同版本的 Array 构造函数

数组的常用方法

迭代器方法
keys():返回数组索引的迭代器
values():返回数组元素的迭代器
entries():返回索引/值对的迭代器
javascript
const a = ["foo", "bar", "baz"];

Array.from(a.keys()); //[0, 1, 2]
Array.from(a.values()); //["foo", "bar", "baz"]
Array.from(a.entries()); //[{0, "foo"}, {1, "bar"}, {2, "baz"}]

//es6的解构可以很容易的拆分键值对
for (const [id, elem] of a.entries()) {
  console.log(id);
  console.log(elem);
}
//0
//foo
//...
改变原数组
1、push(栈方法、队列方法)
javascript
//arr = []
let num = arr.push(1);
//arr = [1]
//num = 1,返回数组最新长度
let num = arr.push(1, 2, 3, 4);
//arr = [1, 1, 2, 3, 4]
//num = 5

//手写push
Array.prototype.push = function () {
  for (var i = 0; i < arguments.length; i++) {
    this[this.length] = arguments[i];
  }
  return this.length;
};
2、pop(栈方法、模拟反向队列)
javascript
//arr = [1, 2, 3]
arr.pop();
//arr = [1, 2]
var num = arr.pop();
//arr = [1]
//num = 2,返回被删除的项
3、shift(队列方法)
javascript
//arr = [1, 2, 3];
arr.shift();
//arr = [2, 3];
var num = arr.shift();
//arr = [3]
//num = 2,返回删除的项
4、unshift(模拟反向队列)
javascript
//arr = []
arr.unshift(0);
//arr = [0]
arr.unshift(1);
//arr = [1, 0]
let num = arr.unshift(-1, 1);
//arr = [-1, 1, 1, 0]
//num = 4,返回数组最新长度
5、reverse
javascript
//arr = [1, 2, 3]
let arr1 = arr.reverse();
//arr = [3, 2, 1]
//arr1 = [3, 2, 1],返回调用它的数组的引用
6、splice

splice(切口下标,长度,往切口添加的数据......)

javascript
//arr = [1, 2, 3, 4, 5, 4]
arr.splice(1, 2);
//arr = [1, 4, 5, 4]
arr.splice(1, 1, 0, 1, 0);
//arr = [1, 0, 1, 0, 5, 4]
arr.splice(4, 0, 3);
//arr = [1, 0, 1, 0, 3, 5, 4]
arr.splice(-1, 1);
//arr = [1, 0, 1, 0, 3, 5]

//return arr
7、sort
javascript
//arr = [1, 4, -1, 0]
let arr1 = arr.sort();
//arr = [-1, 0, 1, 4]
//arr1 = [-1, 0, 1, 4],返回调用它的数组的引用

//return arr

//arr = [1, 3, 5, 4, 10]
arr.sort();
//arr = [1, 10, 3, 4, 5]

//arr = [1, 3, 5, 4, 10]
//1、必须写俩形参
//2、看返回值 return < 0 时,形参前一个在前
//          return > 0 时,形参后一个在前
//          return == 0 时,形参不动
arr.sort(function(a, b) {
    if (a > b) {
        return 1;
    } else {
        return -1;
    }
});
//arr = [1, 3, 4, 5, 10]
//优化
arr.sort(function(a, b) {
    return a - b;
});
//再优化
arr.sort((a, b) => a - b);
//或者
arr.sort((a, b) => a > b ? -1 : a < b ? 1 : 0)

//乱序
arr.sort(fucntion() {
	return Math.random() - 0.5;
});
8、fill
javascript
const a = [0, 0, 0, 0, 0];

a.fill(5); //[5, 5, 5, 5, 5]
a.fil(6, 3); //[0, 0, 0, 6, 6]
//用7填充索引为1-3(不包括3)的位置
a.fill(7, 1, 3); //[0, 7, 7, 0, 0]
//负数为length+value
a.fill(8, -4, -1); //[0, 8, 8, 8, 0]
//索引超出边界则无效,部分未超出边界部分有效
//相反方向的索引无效,例如a.fill(8, 3, 0);
9、copyWithin
javascript
const a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

//复制0开始的内容,插入到5开始的位置
a.copyWithin(5); //[0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
//复制5开始的内容,插入到0开始的位置
a.copyWithin(0, 5); //[5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
//复制0开始3结束的内容,插入到4开始的位置
a.copyWithin(4, 0, 3); //[0, 1, 2, 3, 0, 1, 2, 7, 8, 9]
//被复制的值在复制开始前就已经被JavaScript引擎保存,所以不会出现重复复制的风险
a.copyWithin(2, 0, 6); //[0, 1, 0, 1, 2, 3, 4, 5, 8, 9]
//索引超出边界则无效,部分未超出边界部分有效
//相反方向的索引无效,例如a.copyWithin(8, 3, 0);
不改变原数组
1、concat(受参数数组中 Symbol.isConcatSpreadable 影响,详情请查阅上文)
javascript
var arr = [1, 2, 3, 4, 5];
var arr1 = [7, 8, 9];
var res = arr.concat(arr1);
//res = [1, 2, 3, 4, 5, 7, 8, 9]
2、toString

调用每个值的 toString 方法,然后再拼接,以逗号分隔

javascript
//arr = [1, 2, 3]
arr.toString();
// "1, 2, 3"
3、slice
javascript
//arr = [1, 2, 3, 4, 5]
//slice(从该位开始截取,截取到该位之前),可以是负数
var arr1 = arr.slice(1, 2);
//arr1 = [2]
var arr1 = arr.slice(1);
//arr1 = [2, 3, 4, 5]
var arr1 = arr.slice();
//arr1 = [1, 2, 3, 4, 5]
3、join
javascript
//arr = [1, 2, 3, 4]
var str = arr.join("-");
//str = "1-2-3-4"

join(); //按逗号连
join(undefined); //按逗号连
4、split
javascript
//str = "1-2-3-4"
var arr = str.split("-");
//arr = [1, 2, 3, 4]
5、toLocaleString

调用每个值的 toLoacleString 方法,然后再拼接,以逗号分隔

javascript
//与toString方法没有什么不同,只是如果数组值上的toLocaleString被重写了的话就会不一样了
6、indexOf、lastIndexOf

传入一个或两个参数,第一个参数表示要查找的元素,第二个参数表示起始位置搜索位置,返回元素位置,使用全等(===)进行比较

前者方法从头开始,后者从尾开始

7、includes(es7)

传入一个元素,寻找在数组中是否包含此元素,返回布尔值,使用全等(===)进行比较

8、find、findIndex

这两个函数都使用了断言函数,接收一个或两个参数,第一个参数为断言函数,第二个参数为断言函数的 this,从数组的最小索引开始,find 返回匹配到的第一个元素,findIndex 返回匹配到的第一个元素下标

javascript
const people = [
  {
    name: "Matt",
    age: 27,
  },
  {
    name: "Nico",
    age: 29,
  },
];
people.find((elem, index, array) => elem.age < 28); //{name: "Matt", age: 27}
people.findIndex((elem, index, array) => elem.age < 28); //0
9、every(迭代方法)

接收一个或两个参数,第一个参数传入一个函数(函数参数:item、index、array),第二个参数传入这个函数的运行上下文作用域对象(影响函数 this 的值),对数组每一项都运行传入的函数,如果每一项都返回 true,则这个函数返回 true

10、some(迭代方法)

接收一个或两个参数,第一个参数传入一个函数(函数参数:item、index、array),第二个参数传入这个函数的运行上下文作用域对象(影响函数 this 的值),对数组每一项都运行传入的函数,如果有一项返回 true,则这个函数返回 true

11、filter(迭代方法)

与 every 和 some 基本一样,不过返回值是由函数返回 true 的元素组成的数组

12、forEach(迭代方法)

与 filter 基本一样,对每一项都运行传入的函数,无返回值

13、map(迭代方法)

与 forEach 基本一样,返回每次函数调用的结果构成的数组

14、reduce、reduceRight(归并方法)

接收两个参数,第一个参数为迭代调用的函数(函数参数:上一个归并值、当前项、当前项的索引、数组本身),第二个参数为最初要加上的元素(不为 num 则调用 toString),迭代数组所有的项,在此基础上构建最终返回值

reduce 从第一项开始到最后一项、reduceRight 从最后一项开始到第一项

默认从下标为 1 开始(若数组只有一位元素,则直接返回该元素),pre 会直接返回下标为 0 的元素;传入第二个参数后,无论参数为什么,都会从下标为 0 开始遍历整个数组

javascript
let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => {
  console.log(index);
  return prev + cur;
});
//1 2 3 4
console.log(sum);
//15

let values = [1, 2, 3];
let sum = values.reduce((prev, cur, index, array) => {
  console.log(index);
  return prev + cur;
}, {});
// 0 1 2
console.log(sum);
//[object Object]123
数组的解构
js
const names = ["lsn", "1", "2"];
const [name1, name2, name3] = names;

定型数组

因为 WebGL 的原因,催生出了 Float32Array,是定型数组中可用的第一个”类型“

ArrayBuffer

ArrayBuffer 是所有定型数组以及视图引用的基本单位,他是一个普通的 js 构造函数,可在内存中分配特定数量的字节空间

javascript
let buf = new ArrayBuffer(16);
buf.byteLength; //16

ArrayBuffer 一经创建就不能再调整大小了,但是可以用 slice 复制到新实例中

javascript
let buf1 = buf.slice(4, 12);
buf1.byteLength; //8

类似于 C++中的 malloc(),但是:

malloc()分配失败返回 null,ArrayBuffer()则直接报错

malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制,ArrayBuffer 分配的内存不能超过 Number.MAX_SAFE_INTEGER(2^53 - 1)字节

malloc()调用成功不会初始化实际地址,声明 ArrayBuffer 则会将所有二进制初始化为 0

malloc()分配的内存除非调用 free()或程序退出,否则系统不能再使用,ArrayBuffer 分配的内存可以被当成垃圾回收,不用手动释放

ArrayBuffer 不能通过仅对其引用就读取或写入内容。必须要通过视图,视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据

DataView

第一种允许读写 ArrayBuffer 的视图。这个视图专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其它类型的视图性能也差一点,DataView 对缓冲内容没有任何预设,也不能迭代

必须在对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例。这个实例可以使用全部或部分 ArrayBuffer,而且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置

javascript
const buf = ArrayBuffer(16);

const fullDataView = new DataView(buf);
fullDataView.byteOffset; //0
fullDataView.byteLength; //16
fullDataView.buffer === buf; //true

const halfDataView = new DataView(buf, 0, 8); //byteOffset=0, byteLength=8
halfDataView.byteOffset; //0
halfDataView.byteLength; //8
halfDataView.buffer === buf; //true

const shalfDataView = new DataView(buf, 8); //byteOffset=8, byteLength=剩余长度
shalfDataView.byteOffset; //8
shalfDataView.byteLength; //8
halfDataView.buffer === buf; //true

通过 DataView 读取缓冲,还需要几个组件:

读或写的字节偏移量,可以看成 DataView 中的某种“地址”

使用 ElementType 实现 js 的 Number 类型到缓冲内二进制格式的转换

内存中值的字节序,默认为大端字节序

ElementType

DataView 对储存在缓冲中的数据类型没有预设,所以必须指定 ElementType

es6 支持八种 ElementType

ElementType字节说明等价 C 类型
Int818 位有符号整数signed char
Uint818 位无符号整数unsigned char
Int16216 位有符号整数short
Uint16216 位无符号整数unsigned short
Int32432 位有符号整数int
Uint32432 位无符号整数unsigned int
Float32432 位 IEEE-754 浮点数float
Float64864 位 IEEE-754 浮点数double

每种类型都有 set 和 get 方法,用 byteOffset 定位要读取或写入值得类型,类型可以是互换使用的

javascript
const buf = new ArrayBuffer(2);
const view = new DataView(buf);

//检查第一个和第二个字符
view.getInt8(0); //0
view.getInt8(1); //0
//检查整个缓冲
view.getInt16(0); //0

//设置整个缓冲都为1
//DataView会自动将数据转换为特定的ElementType
//255的二进制表示:11111111(2^8 - 1)
view.setUint8(0, 255);
view.setUint8(1, 0xff);

view.getInt16(0); //-1
字节序

”字节序“是计算机系统维护的一种字节顺序的约定,DataView 只支持两种约定:大端字节序和小端字节序,大端字节序也称为“网络字节序”,意思是最高有效位保存在第一个字节,最低有效位保存在最后一个字节,小端字节序相反

一般来说 js 运行时所在系统的原生字节序决定了如何读取或写入字节,但是 DataView 并不遵守这个约定,DataView 的所有 API 方法默认位大端字节序,但可以接受一个布尔参数,设置 true 可启用小端字节序

javascript
view.setUint8(0, 0x80);
view.setUint8(1, 0x01);
//0x8 0x0 0x0 0x1
//1000 0000 0000 0001

view.getUint16(0); //0x8001	32769
view.getUint16(0, true); //0x0180	384

view.setUint16(0, 0x0004);
//0x0 0x0 0x0 0x4
view.setUint16(0, 0x0004, true);
//0x0 0x4 0x0 0x0

如果 DataView 读写操作没有足够的缓冲区就会报错,抛出 RangeError

javascript
view.getInt16(1); //RangeError
view.getInt16(-1); //RangeError

DataView 会将写入缓冲区里的值尽量转换为适当的的类型,后备为 0,如果无法转化,则抛出错误

javascript
view.setInt8(0, 1.5); //1
view.setInt8(0, [4]); //4
view.setInt8(0, "f"); //0
view.setInt8(0, Symbol()); //TypeError

定型数组

另一种形式的 ArrayBuffer 视图,他特定于一种 ElementType 且遵循系统原生的字节序。并且提供了更广的 API 和更高的性能,定型数组的目的就是为了提高与 WebGL 等原生库交换二进制数据的效率

javascript
const buf = new ArrayBuffer(12);
const ints = new Int32Array(buf);

ints.length;	//3

const ints1 = new Int32Array(6);	//ArrayBuffer是24字节
ints1.length;	//6
ints1.buffer.byteLength;	//24

const ints2 = new Int32Array([2, 4, 6, 8]);
ints2.length;	//4
ints2.buffer.byteLength;	//16
ints2[2];	//6

//复制ints2的值转换为Int16Array
const ints3 = new Int16Array(ints2);
ints3.length;	//4
ints3.buffer.byteLength;	//8
ints2[2];	//6

//通过普通数组创建
const ints4 = new Int16Array.from([3, 4, 5, 6]);
//通过参数创建
const ints5 = new Int16Array.of(3, 4, 5, 6);

定型数组的构造函数都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素大小

javascript
Int16Array.BYTES_PER_ELEMENT; //2
Int32Array.BYTES_PER_ELEMENT; //4

ints1.BYTES_PER_ELEMENT; //4

定型数组默认以 0 填充缓冲

定型数组基本支持所有正常数组方法和属性,并且还有 Symbol.iterator 属性,可以用 for-of 迭代

但是因为定型数组不能修改缓冲区大小,所以对原数组有修改的函数不能使用,如:concat、push、pop

不过定型数组提供了两个方法,向内、向外复制数据:subarray()、set()

set

接收一个或两个参数,第一个参数为要复制的数组,第二个为复制到的起点索引(默认为 0)

javascript
const a = new Int16Array(8);
a.set(Int8Array.of(1, 2, 3, 4)); //[1, 2, 3, 4, 0, 0, 0, 0]
a.set(Int8Array.of(5, 6, 7, 8), 4); //[1, 2, 3, 4, 5, 6, 7, 8]
subarray

执行与 set 相反的操作

javascript
a.subarray(2); //[3, 4, 5, 6, 7, 8]
a.subarray(5, 7); //[6, 7]
上溢和下溢

某一个值的上溢和下溢不会影响到其他索引

类数组

类数组 arguments 没有数组的方法,但是与数组很相似,既能像数组一样用也能像对象一样用,但数组方法要自己添加

javascript
var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  push: Array.prototype.push,
};
//属性要为索引属性(数字)属性,必须要有length属性,最好加上push

var arr = ["a", "b", "c"];

//{0: "a", 1: "b", 2: "c", length: 3, push: ƒ}
obj.push("d");
//{0: "a", 1: "b", 2: "c", 3: "d", length: 4, push: ƒ}

var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  push: Array.prototype.push,
  splice: Array.prototype.splice,
};
//一旦给一个对象加上splice方法后这个对象将会以数组形式存在,但是他还是对象
//Object(3) ["a", "b", "c", push: ƒ, splice: ƒ]

小题

javascript
var obj = {
  2: "a",
  3: "b",
  length: 2,
  push: Array.prototype.push,
};
obj.push("c");
obj.push("d");
//obj = {2: "c", 3: "d", length: 4, push: ƒ}

数组去重

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

Array.prototype.unique = function () {
  var obj = {},
    res = [],
    length = this.length;
  for (let i = 0; i < length; i++) {
    if (!obj[this[i]]) {
      obj[this[i]] = "1";
      res.push(this[i]);
    }
  }
  return res;
};

Map(es6)

与 Object 实现差不多,但之间还是有细微差异

创建 Map

javascript
const m = new Map();

const m1 = new Map([
  ["key", "val"],
  ["key1", "val1"],
]);

const m2 = new Map({
  [Symbol.iterator]: function* () {
    yield ["key", "val"];
    yield ["key1", "val1"];
  },
});

const m3 = new Map([[]]);
m3.has(undefined); //true
m3.get(undefined); //undefined

添加键值对

set 方法会返回映射实例,所以可以连续使用 . 方法

javascript
m.set("first", "Matt").set("second", "oh");

查询键值对

javascript
m.has("first");
m.get("first");

删除键值对

javascript
m.delete("first"); //删除一个键值对

m.clear(); //删除这个映射实例中所有键值对

Map 内部可以使用任何 js 数据类型作为键,Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的

如果键或值为引用值,只要指向的内存地址不变,键值对就能匹配到

但是也有奇怪的问题

javascript
const m = new Map();
const a = 0 / "", //NaN
  b = 0 / "", //NaN
  pz = +0,
  nz = -0;

a === b; //false
pz === nz; //true

m.set(a, "foo");
m.set(pz, "bar");

m.get(b); //foo
m.get(nz); //bar

顺序与迭代

与 Object 类型不同的是,Map 类型会维护键值对的插入顺序,所以可以根据插入顺序执行迭代操作

映射实例可以提供一个迭代器,能以插入顺序生成[key, value]形式的数据。通过 entries()方法(或 Symbol.iterator 属性,它引用了 entries())取得这个迭代器

javascript
const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"],
]);

m.entries === m[Symbol.iterator]; //true

for (let pair of m.entries()) {
  console.log(pair);
}
//[key1, val1]
//[key2, val2]
//[key3, val3]

//因为entries是默认迭代器,所以可以直接对映射实例使用扩展操作,将映射转换为数组
[...m]; //[[key1, val1], [key2, val2], [key3, val3]]
//若不使用迭代器而是使用回调方式,可以用forEach()方法,第一个参数传入函数,第二个参数传入函数的this,可省略第二个参数
m.forEach((val, key) => console.log(`${key} -> ${val}`));
//还可以使用keys和values返回键值和值

当然 Map 也有 values()方法用于迭代 value,并且按照插入顺序

Object 与 Map

内存占用上来说,不同浏览器内存分配实现不同,但是分配固定大小内存,Map 大约可以比 Object 多储存 50%的键值对

插入性能上来说,如果涉及大量插入,Map 性能更佳

查找速度上来说,如果涉及大量查找操作,Object 性能更佳

删除性能上来说,对大多数浏览器引擎来说,Map 的 delete 操作都比插入和查找更快,如果涉及大量删除操作,Map 性能更佳

WeakMap(es6)

弱映射是 Map 的兄弟类型,其 API 也是 Map 的子集,”Weak“代表 js 垃圾回收程序对待”弱映射“中键的方式

弱映射中的键只能是 Object 或继承自 Object 的类型,尝试使用非对象设置键会抛出 TyprError 异常

创建 WeakMap

javascript
const wm = new WeakMap();

const key = { id: 0 };
const wm1 = new WeakMap([[key, "hello"]]);
wm1.get(key); //"hello"

//如果键不为对象,则会抛出异常,导致整个初始化失败
//可以先包装成对象再操作
const key1 = new String("key1");
const wm2 = new WeakMap([[key1, "hello"]]);

添加键值对

与 Map 一样的 set 方法,set 方法返回调用者实例,所以可以连续使用 . 点方法

查询键值对

与 Map 一样的 get、has 方法

删除键值对

与 Map 一样的 delete 方法

弱键

Weak 表示在 WeakMap 中映射的键不属于正式的引用

javascript
const wm = new WeakMap();
wm.set({}, "hello");
//这个键因为没有别的引用,而WeakMap中的键不属于正式引用,所以这个键值在创建完后就可被垃圾回收,垃圾回收后,这个键值对就从弱映射中消失了,成为一个空映射
const container = {
  key: {},
};
wm.set(container.key, "aa");

container.key = null;
//这样做也会产生上述效果

不可迭代键

弱映射不能够迭代,也不能够 clear

使用弱映射

因为使用方法比较抽象,参阅红宝书 p171 例子

Set(es6)

与 Map 特性几乎一摸一样,请参照上文 Map,也使用 SameValueZero 操作,基本上等于全相等比较 ===

创建 Set

和 Map 基本一样

添加值

add()方法,也可连续使用点方法

查询值

has()方法、size(获取元素的数量)

删除值

delete()方法(返回布尔值,表示是否存在要删除的对象)、clear()方法

顺序与迭代

Set 也会维持插入时的顺序

集合实例会提供一个迭代器,能以插入的顺序生成集合的内容。可以通过 values()方法及其别名方法 keys()(或者 Symbol.iterator 属性,他引用 values())

javascript
const s = new Set(["val1", "val2", "val3"]);

s.values === s[Symbol.iterator]; //true
s.keys === s[Symbol.iterator]; //true

for (let value of s.values()) {
}
for (let value of s[Symbol.iterator]()) {
}

因为 values()是默认迭代器,所以可以直接对集合实例使用扩展操作

javascript
[...s]; //["val1", "val2", "val3"]

entries()方法返回一个迭代器,可以按照插入的顺序返回包含两个元素的数组,是集合当前元素重复出现两边

javascript
for (let pair of s.entries()) {
  console.log(pair);
}
//["val1", "val1"]
//["val2", "val2"]

同样的可以调用 forEach 方法,该方法接受两个参数,一是函数,二是 this(改变函数 this 指向),第二个参数可不传

javascript
s.forEach((val, dupval) => console.log(`${val} -> ${dupval}`));
//val1 -> val1

正式定义集合操作

继承 Set,实现自定义方法,详情请见红宝书 p176

WeakSet(es6)

弱集合与弱映射基本一摸一样,所以请参阅上文 WeakMap

弱集合的值只能是 Object 或继承 Object 的类型

创建 WeakSet

添加值

add()

查询值

has()

删除值

delete()

弱值

同 WeakMap 中的弱键一样,弱集合中的值并不属于正式引用

不可迭代值

弱集合不能够迭代,也不能够 clear

使用弱集合

参阅红宝书 p180

迭代与扩展操作

拥有默认迭代器:Array、所有定型数组、Map、Set

所以上述四种对象都能使用 for-of 迭代

这四种类型也都支持扩展操作符,扩展操作符在执行浅复制时特别有用,只需要简单的语法就能复制整个对象

javascript
let a1 = [1, 2, 3];
let a2 = [...a1];

a1 === a2; //false
//也可以构建成数组的部分元素
let a3 = [1, ...a1, 4];

对于期待可迭代的对象元素,只需要传入一个可迭代对象就能够实现复制

javascript
let m1 = new Map([[1, 2]]);
let m2 = new Map(m1);

m1; //1 -> 2
m2; //1 -> 2

浅复制只能复制对象的引用

javascript
let a1 = [{}];
let a2 = [...a1];

a1[0].foo = "bar";
a2[0]; //{foo: "bar"}

上面这些类型还支持 Array.of 和 Array.from 等静态方法

迭代器与生成器

在 js 中,计数循环就是一种最简单的迭代

但这种循环有两个弊端:1、迭代之前需要事先知道如何使用数据结构,2、遍历顺序并不是数据结构固有的

迭代器模式

在这个前提下,推出了迭代器模式,迭代器模式描述了一个方案,即可以将有些结构称为“可迭代对象”,因为他们实现了正式的Iterable接口,而且可以通过 Iterator 消费(实现了Iterable接口的数据结构都可以被实现 Iterator 接口的结构消费)

迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无需知道其关联的可迭代对象的结构,只需要知道如何取得连续的值,这正是 Iterable 和 Iterator 的强大之处

可迭代协议

实现 Iterable 接口(可迭代协议),要求同时具备两种能力:支持迭代的自我识别能力、创建实现 Iterator 接口的对象的能力

在 ECMAScript 中,意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.Iterator 作为键

这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器

javascript
//检测是否实现迭代器工厂函数
let a = 1;
console.log(a[Symbol.iterator]);	//undefined

let str = "abc";
let arr = [1, 2];
let map = new Map().set(1, 2).set(2, 3);
let set = new Set().add(2).add(1);
let els = documents.querySelectorAll('div');

//这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]);	//f values() { [native code] }
...

//这些工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]());	//StringIterator {}
...

实际写代码中不需要显式的调用这个工厂函数,实现可迭代协议的所有类型都会自动接收可迭代对象的任何语言特性

接收可迭代对象的原生语言特性有:for-of 循环、数组结构、扩展操作符、Array.from()、创建集合、创建映射、Promise.all()接收由期约组成的可迭代对象、Promise.race()接收由期约组成的可迭代对象、yield*操作符,在生成器中使用

这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器

javascript
let arr = ["foo", "bar", "baz"];
for (let el of arr) {
  console.log(el);
}
//foo bar baz

//数组结构
let [a, b, c] = arr;
console.log(a, b, c); //foo bar baz

//扩展操作符
let arr2 = [...arr];
console.log(arr2); //["foo", "bar", "baz"]

//Array.from()
let arr3 = Array.from(arr);
console.log(arr3); //["foo", "bar", "baz"]

//Set构造函数
let set = new Set(arr);
console.log(set); //{'foo', 'bar', 'baz'}

//Map构造函数
let pairs = arr.map((x, i) => [x, i]);
console.log(pairs); //[['foo', 0], ['bar', 1], ['baz', 2]]
let map = new Map(pairs);
console.log(map); //Map(3) {'foo'=>0, 'bar'=>1, 'baz'=>2}

如果原型链父类实现了 Iterable 接口,那这个对象也就实现了这个接口

迭代器协议

迭代器是一种一次性使用的对象,迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next()都会返回一个 IteratorResult 对象,其中包含着迭代器返回的下一个值,如果不调用 next()则无法知道迭代器当前位置

iteratorResult 对象包含两个属性:done(布尔值)和 value(可迭代对象的下一个值,或 undefined)

done 为 true 时,表示“耗尽”,即到达迭代对象尾端

迭代器维护者一个可迭代对象的引用,因此会阻止垃圾程序回收这个可迭代对象

javascript
let arr = ["foo", "bar"];

//迭代器工厂函数
arr[Symbol.iterator]; //f values { [native code] }

//迭代器
let iter = arr[Symbol.iterator]();
iter; //ArrayIterator

//执行
iter.next(); //{done: false, value: "foo"}
iter.next(); //{done: true, value: "bar"}
iter.next(); //{done: true, value: undefined}
//只要done为true,后续调用将返回同样的值
iter.next(); //{done: true, value: undefined}

//不同迭代器之间没有关系,独立运行
let iter1 = arr[Symbol.iterator]();

//迭代器并不是对象的某一时刻快照,如果迭代期间某一对象进行改变,则迭代器也会反应相应变化
let iter2 = arr[Symbol.iterator]();
iter2.next(); //{done: false, value: "foo"}

arr.splice(1, 0, "baz");
iter2.next(); //{done: false, value: "baz"}
iter2.next(); //{done: false, value: "bar"}
显式迭代器和原生迭代器
javascript
//实现了可迭代接口iterable
//调用默认的迭代器工厂函数会返回一个实现迭代器接口(iterator)的迭代器对象
class Foo {
  [Symbol.iterator]() {
    return {
      next() {
        return { done: false, value: "foo" };
      },
    };
  }
}
let foo = new Foo();
//实现了迭代器接口的对象
foo[Symbol.iterator](); //{next: f() {}}

//实现了可迭代接口iterable
//调用Array类型默认的迭代器工厂函数会创建一个ArrayIterator的实例
let arr = new Array();
arr[Symbol.iterator](); //Array Iterartor {}
自定义迭代器
javascript
class Counter {
  constructor(limit) {
    this.limit = limit;
  }

  [Symbol.iterator]() {
    let count = 1;
    let limit = this.limit;
    return {
      next() {
        if (count <= this.limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  }
}

let count = new Counter(3);
for (let el of count) {
  console.log(el);
}

这种方式创建的迭代器也实现了 Iterable 接口,Symbol.iterator 引用的工厂函数会返回相同的迭代器

javascript
let arr = [1, 2];
let iter1 = arr[Symbol.iterator]();
let iter2 = iter1[Symbol.iterator]();
iter1 === iter2; //true

所以也可以直接迭代迭代器

javascript
for (let i of arr) {
  console.log(i);
}
// 1 2
for (let i of iter1) {
  console.log(iter1);
}
// 1 2
提前终止迭代器

可选的 return()方法用于提前终止迭代器时执行的逻辑

可能的情况包括:for-of 循环通过 break、continue、return 或 throw 提前退出

结构操作并未消费所有值

javascript
class Counter {
  constructor(limit) {
    this.limit = limit;
  }

  [Symbol.iterator]() {
    let count = 1;
    let limit = this.limit;
    return {
      next() {
        if (count <= this.limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
      return() {
        console.log("exiting early");
        return { done: true };
      },
    };
  }
}

let counter = new Counter(5);
for (let i of counter) {
  if (i > 2) {
    break;
  }
  console.log(i);
}
//1 2 exiting early

try {
  for (let i of counter) {
    if (i > 2) {
      throw "err";
    }
    console.log(i);
  }
} catch (e) {}
//1 2 exiting early

let [a, b] = counter;
//exiting early

如果迭代器没有关闭,则还可以从上次离开的地方继续迭代(比如数组的迭代就是不可关闭的)

javascript
let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
for (let i of iter) {
  console.log(i);
  if (i > 2) {
    break;
  }
}
//1 2 3
for (let i of iter) {
  console.log(i);
}
//4 5

return 方法是可选的,并非所有的迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。但是仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的,因为调用 return 并不会强制迭代器进入关闭状态,但是 return 方法还是会调用

javascript
let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
iter.return = function () {
  console.log("exiting early");
  return { done: true };
};
for (let i of iter) {
  console.log(i);
  if (i > 2) {
    break;
  }
}
//1 2 3 exiting early
for (let i of iter) {
  console.log(i);
}
//4 5

生成器(es6)

生成器是 es6 中一个极为灵活的结构,拥有在一个函数块内暂停和回复代码执行的能力

生成器基础

生成器的形式是一个函数,函数名称前加一个星号(*)表示他是一个生成器,只要是可以定义函数的地方就可以定义生成器(除开箭头函数

javascript
//生成器函数声明
function* feneratorFn() {}
//生成器函数表达式
let generatorFn = function* () {};
//作为对象字面量方法的生成器函数
let foo = {
  *generatorFn() {},
};
//作为实例方法的生成器函数
class foo {
  *generatorFn() {}
}
//作为静态方法的生成器函数
class foo {
  static *generatorFn() {}
}

标识星号不受两侧空格影响

调用生成器函数会产生一个生成器对象,生成器对象一开始处于暂停执行(suspended)状态。生成器对象也实现了 Iterator 接口,具有 next()方法,调用它会让生成器开始或恢复执行

javascript
function* generatorFn() {}
let g = generatorFn();
console.log(g); //generatorFn {<suspended>}
console.log(g.next()); //{done: true, value: undefined}

//value属性是生成器函数的返回值
function* generatorFn() {
  return "foo";
}
let g = generatorFn();
console.log(g.next()); //{done: true, value: "foo"}
console.log(g.next()); //{done: true, value: undefined}

//生成器函数只会在初次调用next()的时候执行
function* generatorFn() {
  console.log("foo");
}
let g = generatorFn();
g.next(); //foo

生成器实现了 Iterable 接口,它们默认的迭代器是自引用的

javascript
function* generatorFn() {}
console.log(generatorFn);
//f* generatorFn() {}
console.log(generatorFn()[Symbol.iterator]);
//f [Symbol.iterator]() {native code}
console.log(generatorFn());
//generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]());
//generatorFn {<suspended>}

let g = generatorFn();
g === g[Symbol.iterator](); //true
通过 yield 中断执行

关键字 yield 可以让生成器停止和开始执行,函数在遇到 yield 关键字之前会正常执行,遇到这个关键字后停止执行,函数的作用域状态会被保留

停止执行的生成器只能通过生成器对象上调用 next()方法来恢复执行

javascript
function* gener() {
  yield;
}
let g = gener();
g.next(); //{done: false, value: undefined}
g.next(); //{done: true, value: undefined}

此时的 yield 有点像函数中间返回语句,他生成的值会出现在 next 方法返回的对象里

通过 yield 关键字退出的生成器函数 done:false,通过 return 关键字推出的函数 done:true

javascript
function* gener() {
  yield "a";
  yield "b";
  return "c";
}
let g = gener();
g.next(); //{done: false, value: "a"}
g.next(); //{done: false, value: "b"}
g.next(); //{done: true, value: "c"}
g.next(); //{done: true, value: undefined}

不同的生成器对象对应的作用域不一样,独立运行(类似于迭代器),互不影响

yield 关键字只能在生成器函数的上下文使用,不然会报错

javascript
function* foo() {
  yield;
}
//true

function* foo() {
  function bar() {
    yield;
  }
}
//false

生成器显式调用 next()作用不大,一般把生成器当成可迭代对象使用起来会更方便

javascript
function* gener() {
  yield 1;
  yield 2;
  yield 3;
}
for (let i of gener()) {
  console.log(i);
}
//1 2 3

//控制循环次数
function* gener(num) {
  while (num--) {
    yield;
  }
}

yield 关键字还可以作为函数中间参数使用,上一次生成器函数暂停的 yield 会接收到传给 next 函数的第一个值,第一次调用 next()传入的值不会被使用,因为第一次调用是为了执行生成器函数

javascript
function* gener(init) {
  console.log(init);
  console.log(yield);
  console.log(yield);
}
let g = gener("foo");
g.next("bar"); //foo
g.next("baz"); //baz
g.next("quz"); //quz

yield 关键字可以同时用于输入和输出

javascript
function* gener() {
  return yield "foo";
}
let g = gener();
console.log(g.next()); //{done: false, value: 'foo'}
console.log(g.next("bar")); //{done: true, value: 'bar'}

必须对整个表达式求值才能确定要返回的值,return 先执行,所以执行表达式(yield ‘foo’)并求出它的值,yield 阻塞 return 的执行,率先返回”foo“,然后 yield 接收到第二次 next()函数的传参,作为参数传给 return

产生可迭代对象

用星号增强 yield,让其迭代一个可迭代对象

javascript
function* gener() {
  for (const i of [1, 2, 3]) {
    yield i;
  }
}
//等价于
function* gener() {
  yield* [1, 2, 3];
}
let g = gener();
for (const x of gener()) {
  console.log(x);
}
// 1 2 3

yield*只是将一个可迭代对象序列化为一串可以单独产出的值,其值为关联迭代器返回 done 为 true 时的 value 属性,对于普通迭代器来说这个值是 undefined,但对于生成器函数产生的迭代器来说,这个值就是生成器函数产生的值

javascript
function* gener() {
  console.log("iter", yield* [1, 2, 3]);
}
for (const x of gener()) {
  console.log(x);
}
//1 2 3
//iter undefined

function* gener() {
  yield* [1, 2, 3];
  return "bar";
}
function* generator() {
  console.log("iter", yield* gener());
}
for (const x of generator()) {
  console.log(x);
}
//1 2 3
//iter bar
实现递归操作
javascript
function* ntime(n) {
  if (n > 0) {
    yield* ntime(n - 1);
    yield n - 1;
  }
}
for (const x of ntime(3)) {
  console.log(x);
}
// 0 1 2
生成器作为默认迭代器
javascript
class Foo {
  constructor() {
    this.val = [1, 2, 3];
  }

  *[Symbol.iterator]() {
    yield* this.val;
  }
}
const f = new Foo();
for (const x of f) {
  console.log(x);
}
//1 2 3
提前终止生成器

与迭代器类似,生成器也支持“可关闭概念”,return 方法可以提前终止生成器,throw 方法也可以,这两个方法都会强制让生成器进入关闭状态

javascript
function* gener() {
  for (const x of [1, 2, 3]) {
    yield x;
  }
}
const g = gener();
console.log(g); //gener {<suspended>}
console.log(g.return(4)); //{done: true, value: 4}
console.log(g); //gener {<closed>}
//与迭代器不同,生成器所有对象都有return方法,只要通过它进入关闭状态就无法恢复了
//后续调用next只会显示done:true,提供任何返回值都不会被储存或传播

//for-of循环等内置语言结构会忽略状态为done:true的IteratorObject内部返回值
function* gener() {
  for (const x of [1, 2, 3]) {
    yield x;
  }
}
const g = gener();
for (const x of g) {
  if (x > 1) {
    g.return(4);
  }
  console.log(x);
}
//1 2

//throw方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭
console.log(g); //gener {<suspended>}
try {
  g.throw("foo");
} catch (e) {
  console.log(e); //foo
}
console.log(g); //{<closed>}
//如果生成器内部处理了这个错误,生成器将不会关闭,而且还可以继续恢复运行。错误处理会跳过对应的yield,因为这个错误会在生成器的yield中被抛出,然后yield外的try/catch捕获
function* gener() {
  for (const x of [1, 2, 3]) {
    try {
      yield x;
    } catch (e) {}
  }
}
const g = gener();
console.log(g.next()); //{done: false, value: 1}
g.throw("foo");
console.log(g.next()); //{done: false, value: 3}
//如果生成器对象还没开始执行,那么调用throw()抛出的错误相当于在外部抛出,不会被内部捕获

期约与异步函数

早期的 js 中只支持回调函数,这个策略不具有扩展性;而现在广泛使用期约与异步函数处理

期约

遵循 Promise/A+规范

创建

使用 new 关键字实例化,创建新期约时需要传入执行器(executor)函数作为参数,如果不提供执行器函数则会抛出 SyntaxError

javascript
let p = new Promise(() => {});
setTimeout(console.log, 0, p); //Promise <pending>
期约状态机

期约有三种状态:待定(pending)、兑现(fulfilled,有时候也称为解决,resolved)、拒绝(rejected)

期约在待定状态下可以落定(settled)为代表成功的兑现状态和代表失败的拒绝状态,无论落定为哪种状态都是不可逆的,而且也不能保证期约必然会脱离待定状态,所以对于这三种状态都应该具有恰当的行为

期约状态是私有的,不能直接通过 js 检测到,而且不能被外部 js 代码修改

解决值、拒绝理由

每个期约只要状态切换为兑现就会产生一个私有的内部值;每个期约状态只要切换为拒绝就会产生一个私有的内部理由;无论是内部值还是内部理由,都是包含原始值或对象的不可修改引用

二者都是可选的,且默认值为 undefined,在期约达到某个落定状态时执行的异步代码始终会收到这个值或理由

通过执行器函数控制期约状态

期约状态是私有的,所以只能通过内部进行操作

执行器函数有两项职责:初始化期约的异步行为和控制状态的最终转换

期约的最终状态转换是通过调用它两个函数参数实现的:

javascript
let p = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p); //Promise <resolved>

let p = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p); //Promise <rejected>

调用了修改状态参数函数后,状态就不可再转换了,继续调用修改状态会静默失败

Promise.resolve()

通过这个实例化静态方法,可以定义一个解决的期约,而不是待定状态

javascript
//下面两个其实是一样的
let p = new Promise((resolve, reject) => resolve());
let p = Promise.resolve();

//期约的解决值对应传给Promise.resolve()的第一个参数
setTimeout(console.log, 0, Promise.resolve());
//Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(1, 2));
//Promise <resolved>: 1
//多余的参数会被忽略

对于静态方法而言,如果传入的参数就是一个期约,那他的行为就类似于空包装,所以 Promise.resolve()是一个幂等方法

javascript
let p = new Promise.resolve(3);
p === Promise.resolve(p); //true
p === Promise.resolve(Promise.resolve(p)); //true

这个幂等函数会保留期约的状态

javascript
let p = Promise(() => {});
setTimeout(console.log, 0, p); //Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); //Promise <pending>

这个方法能够包装任何非期约的值,包括错误对象,并将其转换为解决的期约,所以可能导致不合法的行为

javascript
let p = Promise.resolve(new Error("foo"));
setTimeout(console.log, 0, p);
//Promise <resolved>: Error: foo
Promise.reject()

与 Promise.resolve()类似,它会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能被 try/catch 捕获,只能通过拒绝处理程序捕获)

拒绝期约的理由就是传给 Promise.reject()第一个参数,这个参数也会传给后续的拒绝处理程序

javascript
let p = new Promise.reject(3);
setTimeout(console.log, 0, p);
//Promise <reject>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); //3

但是 Promise.reject()没有等幂逻辑,如果转给他一个期约对象,这个期约对象会成为它拒绝的理由

javascript
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
//Promise <rejected>: Promise <resolved>
同步/异步的二元性

try/catch 方法不能捕获 Promise 的 reject,因为它不是异步模式的捕获

代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——也就是期约方法

期约的实例方法

实现 Thenable 接口

在 es 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了 Thenable 接口

Promise.prototype.then()

这个方法是为期约添加处理程序的主要方法,他最多接收两个参数:onResolved 处理程序和 onRejected 处理程序,两个参数表都是可选的,如果提供则会在期约分别进入 ”兑现“ 和 “拒绝” 状态时进行

javascript
function onResolved(id) {
  setTimeout(console.log, 0, id, "resolved");
}
function onRejected(id) {
  setTimeout(console.log, 0, id, "rejected");
}

let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));

p1.then(
  () => onResolved("p1"),
  () => onRejected("p1")
);
p2.then(
  () => onResolved("p2"),
  () => onRejected("p2")
);

//3 second pass away
//p1 resolved
//p2 rejected

这两个处理程序一定市互斥的,因为期约状态只能转换一次

传给 then()的任何非函数类型的参数都会被静默忽略,如果不想提供某写参数,可以在相应位置上传入 undefined

javascript
p1.then("hello");
//会被静默忽略,不推荐

p2.then(null, () => onRejected("p2"));
//推荐的写法

Promise.prototype.then()返回一个新的期约

javascript
let p1 = new Promise(() => {});
let p2 = p1.then();
setTimeout(console.log, 0, p1); //Promise <pending>
setTimeout(console.log, 0, p2); //Promise <pending>
setTimeout(console.log, 0, p1 === p2); //false

这个新期约是基于 onResolved 的返回值构建。该处理程序的返回值会通过 Promise.resolve()包装来生成新期约;如果没有提供这个处理程序,则 Promise.resolve()会包装上一个期约解决后的值;如果没有显示的返回语句,则 Promise.resolve()会包装默认的返回值 undefined

javascript
let p1 = new Promise.resolve("foo");
let p2 = p1.then();
setTimeout(console.log, 0, p2); //Promise <resolved>: foo

let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());

setTimeout(console.log, 0, p3); //Promise <resolved>: undefined
setTimeout(console.log, 0, p4); //Promise <resolved>: undefined
setTimeout(console.log, 0, p5); //Promise <resolved>: undefined

如果有显式的返回值,则 Promise.resolve()会包装这个值

javascript
let p6 = p1.then(() => "bar");
let p7 = p1.then(() => Promise.resolve("bar"));

setTimeout(console.log, 0, p6); //Promise <resolved>: bar
setTimeout(console.log, 0, p7); //Promise <resolved>: bar

//保留返回的期约
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
//Uncaught (in Promise): undefined

setTimeout(console.log, 0, p8); //Promise <pending>
setTimeout(console.log, 0, p9); //Promise <rejected>: undefined

//抛出异常会返回拒绝的期约
let p10 = p1.then(() => {
  throw "baz";
});
//Uncaught (in Promise): baz
setTimeout(console.log, 0, p10); //Promise <rejected>: baz

//返回的错误值会被包装在一个解决的期约中
let p11 = p1.then(() => Error("quz"));
setTimeout(console.log, 0, p11); //Promise <resolved>: Error: quz

onRejected 处理程序也与之类似:onRejected 处理程序返回的值也会被 Promise.resolve()包装,所以详细信息与上述 onResolved 处理程序一样,只不过调用方法是promise.then(null, () => {})

Promise.prototype.catch()

这个方法用于给期约添加拒绝处理程序,这个方法只接收一个参数:onRejected 处理程序,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onrejected)

该方法返回一个新的期约实例,与 Promise.prototype.then(null, onrejected)返回是一样的

Promise.prototype.finally()

这个方法用于给期约添加 onFinally 处理程序,这个处理程序在期约状态转换的时候会调用;它可以避免 onResolved 和 onRejected 中的冗余代码;但是它不能知道期约的状态是解决还是拒绝,所以这个代码主要用于添加清理代码

该方法返回一个新的期约实例,因为它无法知道期约是解决还是拒绝,所以他会原样后传父期约,无论父期约是解决还是拒绝

如果返回的是一个待定的期约,,或者 onFinally 处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约

javascript
let p = new Promise.resolve("foo");

let p1 = p.finally(() => new Promise(() => {}));
let p2 = p.finally(() => Promise.reject());
//Uncaught (in promise) undefined

setTimeout(console.log, 0, p1); //Promise <pending>
setTimeout(console.log, 0, p2); //Promise <rejected>: undefined

let p3 = p.finally(() => {
  throw "bar";
});
//Uncaught (in promise) bar
setTimeout(console.log, 0, p3); //Promise <rejected>: bar

如果返回待定期约,则期约一解决,新期约还是会原样后传初始期约

javascript
let p = Promise.resolve("foo");

let p1 = p.finally(() => new Promise((resolve, reject) => setTimeout(() => resolve("bar"), 100)));
setTimeout(console.log, 0, p1);
//Promise <pending>
setTimeout(console.log, 200, p1);
//Promise <resolved>: foo
非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行;跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行

这个特性由 js 运行时保证,被称为”非重入“特性

javascript
let p = Promise.resolve();
p.then(() => console.log("onResolved handler"));
console.log("then() returns");

//then() returns
//onResolved handler

then()会把 onResolved 处理程序推进消息队列,但这个处理程序在当前线程上的同步代码执行完成前不会执行

即使状态改变发生在添加处理程序之后,处理程序也会等到运行的消息队列让它出列时才会执行

非重入适用于 onResolved/onRejected 处理程序、catch()处理程序和 finally()处理程序

临近处理程序的顺序

如果一个期约有多个处理程序,当期约状态发生变化时,相关处理程序会按照添加他们的顺序依次执行,无论是 then、catch 还是 finally 添加的处理程序都是如此

javascript
p.finally(() => setTimeout(console.log, 0, "1"));
p.finally(() => setTimeout(console.log, 0, "2"));
//1
//2
传递解决值和拒绝理由

再执行函数中,解决值和拒绝理由分别是作为 resolve()和 reject()的第一个参数往后传递的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数

javascript
let p = new Promise((res, rej) => res("foo"));
p.then((value) => console.log(value)); //foo
拒绝期约与拒绝错误处理

拒绝期约类似于 throw()表达式,因为他们都代表一种程序状态,即需要中断或者特殊处理,在期约的处理程序或执行函数中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由

javascript
let p1 = new Promise((res, rej) => rej(Error("foo")));
let p2 = new Promise((res, rej) => {
  throw Error("foo");
});
let p3 = Promise.resolve().then(() => {
  throw Error("foo");
});
let p4 = Promise.reject(Error("foo"));

//这个期约都返回同样的报错
//Promise <rejected>: Error: foo
//同样的也会抛出四个未捕获错误

期约可以以任何理由拒绝,包括 undefined,但是建议统一使用错误对象,因为错误对象可以让浏览器捕获错误对象中的栈追踪信息,例如:

javascript
Uncaught (in promise) Error: foo
	at Promise (test.html:5)
	at new Promise (<anonymous>)
    at test.html:5

因为期约中抛出错误时,错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令

异步错误只能通过异步的 onRejected 处理程序捕获

这不包括执行函数中的错误,再拒绝或解决期约前,处理函数中仍然可以通过 try/catch 来捕获错误

javascript
let p = Promise.reject(Error("foo")).catch((e) => {});

//错误
try {
  Promise.reject(Error("foo"));
} catch (e) {}

//正确
let p = new Promise((res, rej) => {
  try {
    throw Error("foo");
  } catch (e) {}

  res("bar");
});
setTimeout(console.log, 0, p); //Promise <resolved>: bar

then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch,都是捕获错误后将其隔离,同时不影响正常代码运行

javascript
new Promise((res, rej) => {
  console.log("start");
  reject(Error("foo"));
})
  .catch((e) => {
    console.log("catch error", e);
  })
  .then(() => {
    console.log("finally");
  });
//start
//catch error Error: foo
//finally

期约连锁与期约合成

多个期约组合在一起可以构成强大的代码逻辑

期约连锁

把期约逐个的串联起来是一种非常有效的变成模式。因为期约的实例方法(then()、catch()和 finally())都会返回新的期约对象,而这个期约又有自己的实例方法,这样连缀方法调用就可以构成自己的”期约连锁“

期约图

期约连锁可以构建有向非循环图结构

例如二叉树:

javascript
//		a
//	b		c
//d	 e	   f  g

let a = new Promise((res, rej) => {
  console.log("a");
  resolve();
});
let b = a.then(() => console.log("b"));
let c = a.then(() => console.log("c"));

b.then(() => console.log("d"));
b.then(() => console.log("e"));
c.then(() => console.log("f"));
c.then(() => console.log("g"));

//层序遍历
//a
//b
//c
//d
//e
//f
//g
Promise.all()和 Promise.race()

Promise 提供的两个将多个期约实例组合成一个期约的静态方法

合成后的期约的行为取决于内部期约的行为

Promise.all()

这个方法创建的期约会在一组期约全部解决后再解决,这个静态方法接受一个可迭代对象,返回一个新期约

javascript
let p1 = Promise.all([Promise.resolve(), Promise.resolve()]);

//可迭代对象中的元素会通过Promise.resolve()转换成期约
let p2 = Promise.all([1, 2]);

//空的可迭代对象等价于Promise.resolve()
let p3 = Promise.all([]);

//无效的语法
let p4 = Promise.all();
//TypeError

合成的期约只会在每个包含的期约完成之后才解决,如果其中有一个期约待定,则合成的期约待定;如果其中有一个期约拒绝,则合成的期约也会拒绝;只有所有的期约都解决,合成的期约才会解决,并且解决值就是所有包含期约的解决值的数组,按照迭代器的顺序:

javascript
let p = Promise.all([Promise.resolve(3), Promise.resolve(), Promise.resolve(4)]);
p.then((val) => setTimeout(console.log, 0, val)); //[3, undefined, 4]

如果有期约拒绝,则第一个拒绝的期约将会称为合成的期约的拒绝的理由,之后再拒绝的期约的理由不会再影响合成期约的拒绝的理由。但是,这不会影响所有的包含期约正常的拒绝操作

javascript
//第一个期约的拒绝理由会进入合成期约的拒绝处理程序,但是第二个期约也会静默处理,不会有错误跑掉
let p = Promise.all([
    Promise.reject(3),
    new Promise((res, rej) => setTimeout(reject, 1000));
])
p.catch((reason) => setTimeout(console.log, 0, reason));	//3
Promise.race()

这个方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像,该方法接收一个可迭代对象

javascript
let p1 = Promise.race([Promise.resolve(), Promise.resolve()]);

//可迭代对象中的元素会通过Promise.resolve()转换成期约
let p2 = Promise.race([1, 2]);

//空的可迭代对象等价于new Promise(() => {})
let p3 = Promise.race([]);

//无效的语法
let p4 = Promise.race();
//TypeError

该方法不会对 resolve 和 reject 区别对待,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约

如果有一个期约拒绝,则会和上面说的一样被包装然后返回;但是,和 Promise.all()一样,这并不影响所有的包含期约正常的拒绝操作,其他包含的期约的拒绝操作会被静默处理

串行期约合成

可以将多个函数作为处理程序合成一个连续传值的期约连锁

javascript
function a(x) {
  return x + 2;
}
function b(x) {
  return x + 3;
}
function c(x) {
  return x + 4;
}

function compose(...fns) {
  return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}

let addTen = compose(a, b, c);
addTen(8).then(console.log); //18

期约扩展

很多第三方库实现中具备但是 es 规范却未涉及的两个特性:期约取消和进度追踪

期约取消

可以在现有基础性上进行封装,可以实现取消期约的功能

下面是一个计时器例子:

javascript
class CancelToken {
  constructor(cancelFn) {
    this.promise = new Promise((resolve, reject) => {
      cancelFn(() => {
        setTimeout(console.log, 0, "delay cancelled");
        resolve();
      });
    });
  }
}

const startButton = document.querySelector("#start");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayResolve(delay) {
  setTimeout(console.log, 0, "set delay");

  return new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      setTimeout(console.log, 0, "delay resolve");
      resolve();
    }, delay);

    const cancelToken = new CancelToken((cancelCallBack) => cancelButton.addEventListener("click", cancelCallBack));
    cancelToken.promise.then(() => clearTimeout(id));
  });
}

startButton.addEventListener("click", () => cancellableDelayResolve(1000));
期约进度通知

扩展 Promise 类并为它添加 notify()方法

javascript
class TrackablePromise extends Promise {
  constructor(executor) {
    const notifyHandlers = [];
    super((resolve, reject) => {
      return executor(resolve, reject, (status) => {
        notifyHandlers.map((handler) => handler(status));
      });
    });
    this.notifyHandlers = notifyHandlers;
  }

  notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandler);
    return this;
  }
}

//实例
let p = new TrackablePromise((resolve, reject, notify) => {
  function countDown(x) {
    if (x > 0) {
      notify(`${20 * x}% remaining`);
      setTimeout(() => countDown(x - 1), 1000);
    } else {
      resolve();
    }
  }

  countDown(5);
});

p.notify((x) => setTimeout(console.log, 0, x));
p.notify((x) => setTimeout(console.log, 0, x));
p.then(() => setTimeout(console.log, 0, "complete"));
//80% remaining
//60% remaining
//40% remaining
//20% remaining
//complete

//同时可以添加多个notify处理程序
p.notify((x) => setTimeout(console.log, 0, x));
p.notify((x) => setTimeout(console.log, 0, x));
//80% remaining
//80% remaining
//60% remaining
//60% remaining
//40% remaining
//40% remaining
//20% remaining
//20% remaining

异步函数(es8)

异步函数,也称为“async/await”(语法关键字),是期约模式函数在 es 函数中的应用

因为在期约中访问某些值需要在期约处理程序中进行,不是很方便

为此 es 对函数进行了扩展,为其增加了两个关键字:async 和 await,来解决利用异步结构组织代码的问题

async

用于声明异步函数,它可用于函数声明、函数表达式、箭头函数和方法上

javascript
async function foo() {}
let bar = async function () {};
let baz = async () => {};
class Quz {
  async quz() {}
}

使用该关键字可以让函数具有异步特征,但是总体上其代码仍然是同步求值的。在参数或闭包方面,异步函数仍然具有普通函数的正常行为

javascript
async function foo() {
  console.log(1);
}
foo();
console.log(2);

//1
//2
//foo函数仍然会在后面的指令之前运行

不过,异步函数如果使用了 return 关键字返回了值(没有 return 则返回 undefined),这个值会被 Promise.resolve()包装成一个期约对象,异步函数始终返回期约对象

javascript
async function foo() {
  console.log(1);
  return 3;
  //直接返回一个期约也行
}
foo().then(console.log);
console.log(2);

//1
//2
//3

其实异步函数期待返回一个实现 thenable 接口的对象(实际并不是),常规值也行

如果是实现了 thenable 对象,则这个对象可以由提供给 then()的处理程序”解包“;如果不是,则返回值就被当做已经解决的期约

javascript
async function baz() {
  const thenable = {
    then(callback) {
      callback("baz");
    },
  };
  return thenable;
}
baz().then(console.log);
//baz

async function quz() {
  return Promise.resolve("qux");
}
quz().then(console.log);
//qux

于期约处理程序一样,在异步函数中抛出错误会返回拒绝的期约

javascript
async function foo() {
  console.log(1);
  throw 3;
}
foo().catch(console.log);
console.log(2);

//1
//2
//3

不过拒绝期约的错误不会被异步函数捕获

javascript
async function foo() {
  console.log(1);
  return Promise.reejct(3);
}
foo().catch(console.log);
console.log(2);

//1
//2
//Uncaught (in promise): 3
await

异步函数主要针对不会马上完成的任务,所以需要一种暂停和恢复执行的能力,使用 await 关键字可以暂停异步函数代码的执行,等待期约解决

javascript
async function foo() {
  let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
  console.log(await p);
}

foo();
//3

await 关键字会暂停执行异步函数后面的代码,让出 js 运行时的执行线程。这个于生成器函数中的 yield 是一样的

await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再恢复异步函数的执行

await 关键字于 js 一元操作一样,可以单独使用,也可以在表达式中使用:

javascript
async function foo() {
  console.log(await Promise.resolve("foo"));
}
foo();
//foo

async function foo() {
  return await Promise.resolve("foo");
}
foo().then(console.log);
//foo

async function foo() {
  await new Promise((resolve, reject) => setTimeout(resolve, 1000));
  console.log("foo");
}
foo();
//foo 1 second after

await 关键字期待一个 thenable 接口的对象(但实际上并不要求),常规值也可以

如果是实现了 thenable 对象,则这个对象可以由 await 来”解包“;如果不是,则这个值就被当做已经解决的期约

javascript
async function foo() {
  console.log(await "foo");
}
foo();
//foo

async function foo() {
  const thenable = {
    then(callback) {
      callback("foo");
    },
  };
  console.log(await thenable);
}
foo().then(console.log);

await 等待会抛出错误的同步操作,会返回拒绝的期约

javascript
async function foo() {
  console.log(1);
  await (() => {
    throw 3;
  })();
}
foo().catch(console.log);
console.log(2);

//1
//2
//3

对拒绝的期约使用 await 则会释放(unwrap)错误值(将拒绝期约返回):

javascript
async function foo() {
  console.log(1);
  await Promise.reject(3);
  console.log(4); //这行代码不会执行
}
foo().catch(console.log);
cosnole.log(2);

//1
//2
//3
await 限制

await 关键字只能在异步函数中使用,在同步函数中使用会抛出 SyntaxError

因为异步函数的性质不会扩展到嵌套函数,所以 await 只能直接出现在异步函数的定义中

停止和恢复执行

async/await 中真正起作用的是 await

js 运行时在碰到 await 关键字时,会记录在哪里暂停执行,等到 await 右边的值可以用了,js 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行

因此,即使 await 关键字后面跟着一个立即可用的值,函数其余部分也会被异步求值

javascript
async function foo() {
  console.log(2);
  await null;
  console.log(4);
}

console.log(1);
foo();
console.log(3);

//1
//2
//3
//4

书上 p354 有两个很好的例子,一定要看

异步函数策略

实现 sleep

模拟 java 中类似于 sleep 函数的方法

javascript
async function sleep(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function foo() {
  const t0 = Date.now();
  await sleep(1500);
  console.log(Date.now() - t0);
}
foo();
//1502
利用平行执行
javascript
async function randomDelay(id) {
  const delay = Math.random() * 1000;
  return new Promise((resolve) =>
    setTimeout(() => {
      console.log(`${id} finished`);
      resolve();
    }, delay)
  );
}

async function foo() {
  const t0 = Date.now();
  for (let i = 0; i < 5; i++) {
    await randomDelay(i);
  }
  //相当于写五个await radomDelay(x);
  console.log(`${Date.now() - t0}ms`);
}
foo();
//0
//1
//2
//3
//4
//2219ms

这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成;这样可以保证顺序执行,但是总执行事件会被变长

如果不在意执行顺序,可以一次性初始化所有期约

javascript
async function foo() {
  const t0 = new Date.now();
  const promises = Array(5)
    .fill(null)
    .map((_, i) => randomDelay(i));
  for (let p of promises) {
    await p;
  }
  console.log(`${Date.now() - t0}ms`);
}
foo();
//期约将不会按代码先后顺序执行,
//但是每个await按顺序收到了每个期约的值
async function randomDelay(id) {
  const delay = Math.random() * 1000;
  return new Promise((resolve) =>
    setTimeout(() => {
      console.log("${id} finished");
      resolve(id);
    }, delay)
  );
}
async function foo() {
  const t0 = new Date.now();
  const promises = Array(5)
    .fill(null)
    .map((_, i) => randomDelay(i));
  for (let p of promises) {
    console.log(`awaited ${await p}`);
  }
  console.log(`${Date.now() - t0}ms`);
}
foo();
//id finished
//...(不会按顺序)
//awaited 0
//awaited 1
//awaited 2
//awaited 3
//awaited 4
//xxxx ms
串行执行期约

前面我们使用过串行执行期约可以利用 async/await 简化

javascript
async function addTwo(x) {
  return x + 2;
}
async function addThree(x) {
  return x + 3;
}
async function addFive(x) {
  return x + 5;
}
async function addTen(x) {
  for (let fn of [addTwo, addThree, addFive]) {
    x = await fn(x);
  }
  return x;
}
addTen(9); //19
栈追踪于内存管理

期约与异步函数的功能有相当程度的重叠,但他们在内存中的表示则差别很大

看看拒绝期约的栈追踪信息:

javascript
function fooPromiseExecutor(resolve, reject) {
  setTimeout(reject, 1000, "bar");
}
function foo() {
  new Promise(fooPromiseExecutor);
}
foo();
//Uncaught (in Promise) bar
//setTimeout (async)
//fooPromiseExecutor
//foo

按理来说这些函数已经返回了,因此不应该看到它们

其实这是因为 js 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时,调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中,这也意味着栈追踪信息会占用内存,从而带来一些计算和储存成本

如果使用异步函数:

javascript
function fooPromiseExecutor(resolve, reject) {
  setTimeout(reject, 1000, "bar");
}
async function foo() {
  await new Promise(fooPromiseExecutor);
}
foo();
//Uncaught (in Promise) bar
//foo
//async function (async)
//foo

这样子就准确反映了当前调用栈,因为 fooPromiseExecutor 已经被返回了,所以它不存在错误信息中,但是 foo()被挂起了,并没有退出

js 运行时可以简单的在嵌套函数中存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上储存在内存中,可用于在出错时生成栈追踪信息

日期对象(Date)

var date = new Date();,这个 date 是创建时刻的时间,并不会变化

Date 类型保存的是与纪元时间(1970 年 1 月 1 日零时)的毫秒差,并且储存

Date.parse()

可以传入字符串参数,创建字符串参数对应的日期和时间对象

字符串格式:

month/day/year

monthEnglishName day, year

weekdayEnglishName monthEnglishName day year hours:minutes:seconds time-zone,”Tue May 23 2019 00:00:00 GMT-0700“

YYYY-MM-DDTHH:mm:ss.sssZ,”2019-05-23T00:00:00“

Date.UTC()

传入多个数值参数,创建参数对应的时期和时间的对象

参数:年、零起点月数(0 代表 1 月)、日(1~31)、时(0~23)、分、秒

年和月必须传,其他不传默认最小值

new Date(Date.UTC(2019, 1, 1, 2))

Date.now()

返回方法执行时日期和时间的毫秒数

继承的方法

Date 类型重写了toLocaleString()toString()valueOf()

date.toLocaleString()方法返回与浏览器运行的本地环境一致的日期和时间(具体格式根据浏览器而不同)

date.toString()方法返回带时区的日期和时间格式,时间为 24 小时制

date.valueOf()方法返回的是日期的毫秒表示,因此比较操作符可以直接比较其大小,因为会调用其valueOf()方法

格式化方法

Date 类型有专门用于格式化日期的方法,都会返回字符串

date.toDateString(),显示日期中的周几、月、日、年(格式特定于实现)

date.toTimeString(),显示日期中的时、分、秒和时区(格式特定于实现)

date.toLocaleDateString(),显示日期中的周几、月、日、年(格式特定于实现和地区)

date.toLocaleTimeString(),显示日期中的时、分、秒(格式特定于实现)

date.toUTCString(),显示完整的 UTC 日期(格式特定于实现)

1、Date(),返回当前时间信息

传入参数必须为毫秒表示参数,表示为纪元时间之后的毫秒数

可以传入字符串Date("xxxx xx xx")和数值参数Date(2019, 1),其本质调用的是Date.parse()方法和Date.UTC()方法返回相应时间的毫秒数,按照传参决定

2、date.getDate(),返回 date 日

3、date.getDay(),返回 date 星期(星期日为 0)

4、date.getMonth(),返回 date 月份-1

5、date.getFullYear(),返回 date 年份

6、date.getYear(),请使用 getFullYear 来代替

7、date.getHours(),返回 date 小时

8、date.getTime(),返回 date 与纪元时间(1970 年 1 月 1 日零时)的毫秒差

等等还有很多方法:setFullYear()toString()......,这里就不多描述了,请翻阅红宝书(p106)或上网查阅

定时器

var timer = setInterval(function, time);

timer 是一个数值作为这个 interval 的唯一标识

time 在 setInterval 中只读取一次,后面改变 time 值也无效

interval 里 function 中的 this 指向的是 window(除箭头函数外)

!! 因为底层原因,定时器并不准确,并不会真正按照 1000ms 执行 !!

image-20210531151232834

当 interval 里的 function 用字符串代替也行,比如 setInterval("console.log('a')", 1000) 这样也会执行在控台打印 a

var timer = setTimeout(function, time)

timer 是一个数值作为这个 timeout 的唯一标识

timeout 里 function 中的 this 指向 window(除箭头函数外)

这里还有一种用法,就是可以传更多参数给 setTimeout,然后这些参数会传入 setTimeout 的第一个参数函数中

javascript
function fun(...args) {
    console.log(arguments);
}
setTimeout(fun, time, arg1, arg2, ...);	//[arg1, arg2, ...]

clearInterval()

clearTimeout()

es5 严格模式

现在浏览器一般是基于 ese3.0 + es5.0 新增方法 使用的

产生冲突部分一般采用 es3.0 方法

但是在 es5.0 严格模式下,es3.0 和 es5.0 产生冲突的部分将会摒弃 es3.0 不规范的方法而采用 es5.0 方法

开启 es5.0 严格模式方法

在代码逻辑的最上方写

javascript
"use strict";
//全局的最顶端,将会在全局启用

function test() {
  "use strict";
  //局部的最顶端,将会在局部启用,不影响全局
}
使用了 es5.0 严格模式后,将会有以下方法限制
1、arguments.callee
2、func.caller
3、with(obj) {}
4、变量赋值前必须声明
5、局部的 this 必须被赋值,赋值什么就是什么
6、不重复的属性和参数
7、eval()能改变作用域
javascript
//es3.0 都不能用eval(); eval是魔鬼
var a = "123";
eval("console.log(a)");

var global = 100;
function test() {
  global = 200;
  eval("console.log(global)");
}
test();
// 200

function test() {
  var global = 200;
  eval("global = 100;console.log(global);");
  console.log(global);
}

// 100
// 100

正则表达式 RegExp

匹配特殊字符或有特殊搭配原则的字符的最佳选择

转义字符 \

行结束 \r
换行 \n
Table \t

多行字符串代码

1、``
javascript
var test = `fadsfa
asdfadfasf
asdf`;
2、转义字符
javascript
var test =
  "fahskjdfhasf\
发生放大是发\
dasfaf";

创建正则表达式

javascript
var reg1 = /abc/i;
var reg2 = new RegExp("abc", "i");
var reg3 = new RegExp(reg1);
var reg4 = RegExp(reg1); //reg4会成为reg1的引用

//RegExp也可以基于原有的表达式并修改他们的关系
var reg5 = new RegExp(reg1, "g"); // -> /abc/g

使用正则表达式

javascript
str.match(reg); //返回匹配的片段数组
reg.test(str); //返回true或false
reg.exec(str); //返回匹配的片段数组,并确定其位置
模式标记

/abc/i -> 忽略大小写

/abc/g -> 全局匹配

/abc/m -> 多行匹配

/abc/y -> 粘附模式

/abc/u -> Unicode 模式,启用 unicode 匹配

/abc/s -> dotAll 模式,表示元字符.匹配任何字符(包括\n 和\r)

修饰符

/^abc/ -> 开头匹配

/ab|bc/ -> 匹配 ab 或 bc

方括号

/[0-9][123]/ -> 匹配第一位数字在 0-9 区间,第二位数字在 1、2、3 范围内的俩个相邻数

/[abc][c][b]/ -> 匹配第一位字符在 a、b、c 范围内,第二位字符为 c,第三位字符为 b 的三个相邻字符

/[0-9A-z][cd]/g -> 混合模式,按照 ASCII 码顺序,第一位匹配 0-9、A-z 的数字或字符,但是 A-z 中间还有很多字符也夹杂其中

/[^a][^b]/ -> ^ 代表非

圆括号

/(abc|bcd)/ -> 匹配 abc 或 bcd,| 代表或

/(\w)\1/g -> \1代表引用第一个子表达式里的内容,所以会匹配出如 XX 形式的字符串,例如"aac".match(/(\w)\1/g),就是匹配的 aa 字符串

"aaab.match(/(\w)\1\1/g)" -> aaa"aabb".match(/(\w)\1(\w)\2/g) -> aabb

在 exec 里会将子表达式添加进类数组里

image-20210605180600511

match不启用全局匹配的话和上述一样

"aabb".match(/(\w)\1(\w)\2/);

image-20210605181059897

加上全局匹配的话

image-20210605181139696

元字符
javascript
. === [^\r\n]
\w === [0-9A-Za-z_]
\W === [^\w]
\d === [0-9]
\D === [^\d]
\s === [\t\n\r\v\f ]   空格就是空格
\S === [^\s]
\b === 单词边界		|abc| |ccd|	'|'就代表单词边界
\B === 非单词边界
\uxxxx === 以十六进制数规定的Unicode字符

"ab cde ff".match(/\bcde/g); -> cde
"ab cde ff".match(/\bb/g); -> null

\0 === NULL字符
量词
javascript
n+ === [1-∞]个n字符串
n* === [0-∞]个n字符串

"abc".match(/\w*/g); -> ["abc",""]

n? === 0或1个n字符串
n{X} === X个n字符串
n{X,Y} === 包含X至Y个的n字符串
n{X,} === 至少X个的n字符串
n$ === 任何结尾为n的字符串
^n === 任何开头为n的字符串
?=n === 任何其后紧接指定字符串 n 的字符串
?!n === 任何其后没有紧接指定字符串 n 的字符串

除去以上方法外“?”会取消贪心匹配,如
a?? 则只取0个a
a+? 则只取一个a

"abcabc".match(/^abc$/g); -> null	其实固定了只能是字符串"abc"
检验字符串首尾是否有数字	/(^\d|\d$)/g
检验字符串首尾是否都有数字	/^\d[\s\S]*\d$/g
RegExp 实例属性
属性描述
global布尔值,RegExp 对象是否具有标志 g
ignoreCase布尔值,RegExp 对象是否具有标志 i
unicode布尔值,RegExp 对象是否具有标志 u
sticky布尔值,RegExp 对象是否具有标志 y
lastIndex一个整数,标示开始下一次匹配的字符位置
multiline布尔值,RegExp 对象是否具有标志 m
dotAll布尔值,RegExp 对象是否具有标志 s
source正则表达式的字面量字符串(不包括标记部分),没有开头和结尾的斜杠
flags正则表达式的标记字符串
javascript
var reg = /^\s/g;
reg.ignoreCase;
-> false

reg.source	->	"^\s"
reg.flags	->	"g"
RegExp 实例方法
方法描述
compile编译正则表达式
exec检索字符串中指定的值。返回找到的值,并确定其位置
test检索字符串中指定的值。返回 true 或 false
toString返回正则表达式的字面量字符串,如“/abc/g”
toLocaleString与 toString 方法一样
valueOf返回正则表达式本身(不是字符串)
javascript
/^a/g.exec("abc");

->
["a", index: 0, input: "abc", groups: undefined]
0: "a"
groups: undefined
index: 0
input: "abc"
length: 1
__proto__: Array(0)

var reg = /ab/g;	//如果没有全局标记,无论调用多少次exec也只会返回第一个匹配信息
var str = "abababab";
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));

->
["ab", index: 0, input: "abababab", groups: undefined]
["ab", index: 2, input: "abababab", groups: undefined]
["ab", index: 4, input: "abababab", groups: undefined]
["ab", index: 6, input: "abababab", groups: undefined]
null
//可以理解为游标一直在移动
reg.lastIndex	->	游标位置,可修改

//如果设置了粘附标记,每次调用exec只会在lastIndex上寻找匹配项,粘附标记覆盖全局标记
let str = "cat, bat";
let reg = /.at/y
reg.exec(str);	//index:0, "cat", lastIndex:3
reg.exec(str);	//null, lastIndex:0
//因为以索引为3的字符开头找不到匹配项(,被强制作为第一个字符),lastIndex被设置为0
reg.lastIndex = 5;
reg.exec(str);	//index:5, "bat", lastIndex:8

let reg = /abc/g;
console.log(reg.toString());
console.log(typeof reg.toString());
console.log(reg.valueOf());
console.log(typeof reg.valueOf());

->
/abc/g
string
/abc/g
object

image-20210605173641321

image-20210605173702216

RegExp 构造函数属性
全名简写说明
input$_最后搜索的字符串
lastMatch$&最后匹配的文本
lastParen$+最后匹配的捕获数组
leftContext$`input 字符串中出现在 lastMatch 前面的文本
rightContext$'input 字符串中出现在 lastMatch 后面的文本

这些属性可以提取出 exec()和 test()执行操作的相关信息

javascript
let text = "this has been a short summer";
let reg = /(.)hort/gm;

if (reg.test(text)) {
  console.log('"' + RegExp.input + '"'); //"this has been a short summer"
  console.log('"' + RegExp.leftContext + '"'); //"this has been a"
  console.log('"' + RegExp.rightContext + '"'); //" summer"
  console.log('"' + RegExp.lastMatch + '"'); //"short"
  console.log('"' + RegExp.lastParen + '"'); //"s"
}

reg.lastIndex = 0;

if (reg.test(text)) {
  console.log('"' + RegExp.$_ + '"'); //"this has been a short summer"
  console.log('"' + RegExp["$`"] + '"'); //"this has been a"
  console.log('"' + RegExp["$'"] + '"'); //" summer"
  console.log('"' + RegExp["$&"] + '"'); //"short"
  console.log('"' + RegExp["$+"] + '"'); //"s"
}

RegExp 还会存储最多 9 个捕获组匹配项(即圆括号里的匹配字符串),通过属性 RegExp.$1~RegExp.$9 来访问,调用 exec()和 test()时,这些属性会被填充

javascript
let text = "this has been a short summer";
let reg = /(..)or(.)/g;

if (reg.test(text)) {
  console.log(RegExp.$1); // "sh"
  console.log(RegExp.$2); // "t"
}

RegExp 构造函数的属性都没有任何 Web 标准出处,所以不要在生产环境中使用他们

string 对象方法
方法描述
search检索与正则表达式相匹配的值
match找到一个或多个正则表达式的匹配
replace替换与正则表达式匹配的子串
split把字符串分割为字符串数组

search:返回匹配到第一个字符串起始位置,未匹配到返回-1

split:按照匹配到的字符串拆分,但是会将子表达式添加到类数组中

image-20210605181647371

replace:未写正则表达式全局的话只匹配一次,replace(reg, str);

!!!在 str 里可以用$1$2,来引用 reg 里的子表达式

javascript
//如果要把aabb转换成bbaa形式

var str = "aabb";
var reg = /(\w)\1(\w)\2/g;
str.replace(reg, "$2$2$1$1");
//或者
str.replace(reg, function ($, $1, $2) {
  return $2 + $2 + $1 + $1;
});
//$ -> 原字符串:"aabb"

//如果你想在替换字符里使用'$'字符的话,就用'$$',否则'$'会代表匹配到的字符串
//$&	最后匹配的文本,和RegExp.lastContent一样
//$'	匹配的字符串之前的字符串,和RegExp.rightContent一样
//$`	和RegExp.leftContent一样
正向预查(正向断言)、非正向预查
javascript
var str = "abaaaaaa";
var reg = /a(?=b)/g;	匹配a后面紧接b的那个a
str.match(reg);
->
["a"]

var reg = /a(?!b)/g;
str.match(reg);
->
["a", "a", "a", "a", "a", "a"]
练习
javascript
//the-first-name
//theFirstName

var reg = /-(\w)/g;
var str = "the-first-name";
str.replace(reg, function ($, $1) {
  return $1.toUpperCase();
});

//字符串去重
var str = "aaaaaaabbbbbbbccccc";
var reg = /(\w)\1*/g;
str.replace(reg, "$1");

//科学计数法
var str = "100000000000000";
var reg = /(?=\B(\d{3})+$)/g;
str.replace(reg, ".");

总的来说 ECMAScript 在此就告一段落,以上的知识点很重要