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、轮转时间片:两个任务都需要执行时
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就是全局的域
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:undefinedwindow.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对象是引用值的时候,可以修改引用值内部属性,但是不能修改对象的引用,例如:
const person = {};person.name = 'lsn';// correctconst name = 1;name = 2;// wrong
const的作用域与let相同,只在声明所在的块级作用域内有效,并且也是和let一样不可以重复进行声明,不能在定义前访问变量
const在全局作用域中声明的变量不会成为window的对象属性
多个赋值规范:
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_INFINITY
和Number.POSITIVE_INFINITY
可以获取到-Infinity
和Infinity
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中这些转义序列只表示一个字符
字符串是不可变的,一旦创建其值就不能改变了,例如:
var lang = "abc";lang = lang + "bb";//首先会分配一个可以容纳5个字符的空间,然后将abc和bb填入,最后销毁“abc”和“bb”
toString():几乎所有对象都有toString()
方法,除了null和undefined
模板字面量:用`囊括的字符串,模板字面量会保留反引号中所有的空格回车,模板字面量不是字符串,而是一种特殊的JavaScript语法表达式,只不过求值后得到的是字符串,模板字面量可以插值,例如:
// beforelet str = value + " hello " + (value2 * 2);// nowlet str = `${ value } hello ${ value2 * 2 }`;//所有插入的值都会用toString()强转为字符串型,而且任何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);
因为表达式的参数是可变的,通常我们都会使用剩余操作符将他们收集到一个数组中,如:
function test(strings, ...expressions) { for (const expression of expressions) { conosle.log(expression); }}// 6 5 11
如果要获取原字符串:
function test(strings, ...expressions) { return strings[0] + expressions.map((val, index) => `${ val }${ strings[i + index] }`).join('');}
原始字符串:
console.log(`\u00A9`); //©console.log(String.raw`\u00A9`); //\u00A9console.log(`asdf \n aaa`); //会换行console.log(String.raw`asdf \n aaa`); //不会换行console.log(`afdaaaa`); //会换行conaole.log(String.raw`afdaaaa`); //会换行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();
,同时可以传入一个字符串用作对该符号的描述,将来可以通过这个字符串调试代码,但是这个字符串参数与符号定义或标识完全无关。
let a = Symbol();let b = Symbol();console.log(a == b); //falselet c = Symbol('foo');let d = Symbol('foo');console.log(c == d); //falseconsole.log(c); //Symbol(foo);
利用symbol的唯一性可以把它当作对象属性,因为symbol的普通定义是不会有重复的。
**全局符号注册表:**如果不同部分需要用同一个符号实例,可以用一个字符串作为键在全局符号注册表中创建并使用符号:Symbol.for('foo')
,每次调用此方法都会在整个全局运行时注册表中检查,如果有相同键值的则返回同一个实例,如果没有则创建新的实例并返回。不过全局注册表中定义的符号还是不等于Symbol()
定义的符号。
全局注册表中的符号必须用字符串键来创建
let a = Symbol.for('foo');let b = Symbol.for('foo');a === b; //truelet c = Symbol('foo');c === a; //false
作为参数传给Symbol.for()
的任何值都会被转换为字符串(调用toString方法),同时也会作为该符号的描述
let b = Symbol.for('foo');let c = Symbol.for(new Object());console.log(b, c);console.log(new Object().toString());
Symbol.keyFor()
:
用来查询全局注册表,传入参数为符号,返回符号对应的字符串键,如果查询的不是全局符号则返回undefined,如果查询的不是符号则返回TypeError报错
Symbol.keyFor(Symbol.for('foo')); //foo
**使用符号作为属性:**凡是可以使用字符串或数值作为属性的地方都能够使用符号,包括了对象字面量属性和Object.defineProperty()
/Object.definedProperties()
定义的属性,对象字面量只能在计算属性语法中使用符号作为属性。
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()
会返回包有常规属性和符号属性描述符的对象(返回键值对对象)
Reflect.ownKeys()
会返回两种类型的键(键数组)
["a", "b", Symbol(foo), Symbol(bar)]
因为符号是对内存中符号的一个引用,所以直接创建并作用属性的符号并不会丢失,如果没有显示的保存对这些属性的引用,就必须遍历对象所有符号属性才能找到相应的属性键。
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
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操作符使用
function Foo() {};let f = new Foo();f instanceof Foo; //trueclass 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会在原型链上寻找这个属性的定义,和在原型链上寻找其他属性一样,所以可以在继承的类上通过静态方法重新定义这个函数
class Bar();class Baz extends Bar { static [Symbol.hasInstance] () { return false; }}let b = new Baz();Bar[Symbol.hasInstance] (b); //trueb instanceof Bar; //trueBaz[Symbol.hasInstance] (b); //falseb instanceof Baz; //false
Symbol.isConcatSpreadable
该属性表示:一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素
es6中的Array.prototype.concat()方法会根据就接收到的对象类型选择如何将一个类数组对象拼接成数组实例,覆盖Symbol.isConcatSpreadable的值可以修改这个行为
该值为true或真值的时候会将类数组对象打平到末尾,非类数组对象忽略;其值为false或假值的时候将会将整个对象直接追加到整个数组末尾
let ini = ['foo'];let arr = ['bar'];console.log(arr[Symbol.isConcatSpreadable]); //undefinedini.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()方法的字符串实例,返回值没有限制。
class Foomatcher { static [Symbol.match](target) { return target.includes('foo'); }}'foobar'.match(Foomatcher); //true'bar'.macth(Foomatcher); //falseclass 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方法的字符串实例和替换的字符串
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.search
该符号特性与Symbol.match和Symbol.replace一样,Symbol.search函数接收一个参数,为调用search方法的字符串实例
class FooSearch { static [Symbol.search](target) { return target.indexOf('foo'); }}'foo'.search(FooSearch); //0class 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)方法,可以覆盖新创建的实例的原型定义
class Bar extends Array {};class Baz extends Array { static get [Symbol.species]() { return Array; }}let bar = new Bar();bar instanceof Bar; //truebar instanceof Array; //truebar = bar.concat('bar');bar instanceof Bar; //truebar instanceof Array; //truelet baz = new Baz();baz instanceof Baz; //truebaz instanceof Array; //truebaz = baz.concat('baz'); //concat会创建一个baz对象的副本baz instanceof Baz; //falsebaz instanceof Array; //true
Symbol.split
这个方法与前面Symbol.match等三个方法很类似,Symbol.split方法接收一个参数,就是调用split()方法的字符串本身
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),可以控制返回值的原始值
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()使用
class Foo {};let foo = new Foo();console.log(foo); //Foo()console.log(foo.toString()); //[object Object]console.log(foo[Symbol.toStringTag]); //undefinedclass 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就可以开启
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其实是指向数组对象的一个指针
引用值为堆内存中存储
值的改变和复制
var num = 100;var num1 = num;num = 200;
var arr = [1,2];var arr1 = arr;var arr = [1,3];
类型转换
var num = 1 * "1";var num = "1" * "2";var num = "2" - "1";var num = "2" / "1";var num = "1" + "1";类型 值number : 1number : 2number : 1number : 2string : "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()
var num = 123;var str = num.toString();"123"
toString(radix):以10进制为基础转换成radix进制
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
+:加号
*、/、-、%:数学运算
&&、||、!:逻辑运算符
<、>、<=、>=:比较运算符
==、!=:判断运算符
undefined > 0; falseundefined < 0; falseundefined == 0; falseundefined != 0; truenull > 0; falsenull < 0; falsenull == 0; falsenull != 0; trueundefined == null; trueNaN == NaN; false//undefined和null不与数字进行比较,<、>、==、<=、>=输出结果全是false,!=输出结果为true//引用值比较的是地址{} == {} falsevar obj = {}; obj1 = obj; obj == obj1 trueobj === 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位整数储存格式是不可见的。
如同计组讲的,有符号整数采用最左一位充当符号位,负数采用其正数的补码(二补数)表示
将负数转换为二进制时,转换过程中会求得二补数,然后再以更符合逻辑的形式表示。
let num = -18;num.toSring(2); //-10010
ECMAScript中基本所有整数都为带符号的32位整数,不过也有无符号数。
ECMAScript中对数值应用位操作符时。后台:64位数值 -> 32位数值 -> 操作 -> 64位数值,这个转换导致了一个奇特的副作用,就是NaN和Infinity在位操作中都会被当作0处理;位操作处理非数值时,会先用Number()函数将其转换为数值再应用位操作
按位非
~
:返回数值的一补数
let num1 = 25; //00000019let num2 = ~num1; //ffffffe6num2; //-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');
(短路语句)
再扩展一下,通常前端会收到后端的传值,但是我们一般要进行判断该值是否有效,所以我们可以如此:
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; //false
、null === undefined; //false
(数据类型不相同)
条件操作符
三目运算符
1 > 0 ? 2 + 2 : 1; => 4var 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引擎会在后台为每个迭代循环声明一个新的迭代变量,例如:
for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 0);}// 5 5 5 5 5for (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则不执行循环体
let arr = {hello: '1'};for (let i in arr) { console.log(i);}//hello
for-in遍历数组时(不建议用for-in迭代数组),i代表的是index
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里可以和标签语句配合使用,例如:
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也一样
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语句
switch () { case : //.... break; case : //.... break; default : //....}//注意每个case里的break,如果不加break,将会把以下所有case全部执行完。default位置不一定在最后,但是switch如果能够看到default将会直接执行。
ECMAScript赋予switch语句新的特性,就是case判断可以使用表达式,因为case会比较每个条件值,而且是使用全等判断,如case "10":
如果你传入的是数值10
则不会进入此case
存储类型
数组[]
、对象{}
对象由键值对组成,属性名对应一个属性值
var obj = { name : "lsn", age : 19, none : undefined, handsome : true,}取值、赋值:var name = obj.name;obj.name = "lll";
鉴别数据类型
typeof()
var num = 123;console.log(typeof(num)); numbervar 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函数
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(鉴别引用值类型,由原型链决定)
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< truevar obj = {};> obj instanceof Array< false
所有的引用值都是Object的实例,所有的原始值都不是对象,所以用instanceof检测原始值都会返回false
利用toString call
Object.prototype.toString.call([]);//Object.prorotype.toString = function () { //this 谁调用的这个函数,这个this就指向谁//}
函数Function
函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,和其他引用类型一样
可以将函数名想象成指针,函数想象成对象
注意,严格模式下函数有以下规定:
函数不能以eval或arguments作为名称,同样他们俩也不能做参数名,函数参数不能同名
//函数声明//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
参数、arguments
实参在函数里被存在列表arguments(类数组对象)里,形参和实参没有强制规定个数
function abc(a){ console.log(arguments);}abc(1,2,3);arguments = [1,2,3] //实参列表
在形参和列表arguments存在一种映射,某个值改变相应的另一个值也改变
但是形参和arguments在内存中是分开的
function sum(a,b){ a = 2; console.log(arguments[0]); arguments[0] = 4; console.log(a);}sum(1,3);24
但是如果函数一开始就没有存在的映射并不会有此效果
function sum(a,b){ b = 2; console.log(arguments[1]);}sum(1); //只传一个实参情况undefined
严格模式下,给arguments赋值不会再影响形参的值;在函数中尝试重写arguments对象会导致语法错误
function sum(a,b){ a = 2; console.log(arguments[0]); arguments[0] = 4; console.log(a);}sum(1,3);12
arguments.callee
指向arguments对象函数所在的指针
function test(num) { if (num < 1) { return 1; } else { return num * test(num - 1); }}//这样只能调用名称为test的函数,如果函数名变化就会出现相应问题let trueTest = test;test = function() {return 0;};test(5); //0trueTest(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); //0trueTest(5); //120
默认参数值
es5.1以前需要手动检测某个参数是否为undefined,es6之后就能显式定义默认参数了
但是这样将会断开形参与arguments之间的同步映射(只要有一个形参有默认值就会这样)
function test(name = 'lsn') { console.log(name); console.log(arguments[0]);}//给函数传undefined相当于没有传值,也就是arguments不会和name建立联系test(undefined);//lsn//undefinedfunction 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是一样的
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';//}//上面这种会报错
扩展参数
使用扩展操作符会将可迭代对象拆分,并将迭代返回值每个单独传入
function test() {console.log(arguments.length)};test([0, 1]); //1test(...[0, 1]); //2
收集参数
正好和扩展参数相反,会得到一个Array实例
但是不影响arguments
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所指向的对象),并且箭头函数的this是不可改的
window.color = 'red';let o = { color: 'blue'};function say() {console.log(this.color)};say(); //redo.say = say;o.say(); //bluelet say1 = () => {console.log(this.color)};say1(); //redo.say1 = say1;o.say1(); //red
传递参数
因为原始值和引用值存储方式不一样,函数中传参方式有区别,原始值为按值传递,即复制一份副本传入到参数中;而引用值则会将引用值的堆内存位置复制到参数中。
两者其实和复制值是一样的,上文有解释
arguments(ES5严格模式下不允许使用)
1、callee
能够调用当前function(严格模式下访问会报错)
function test () { console.log(arguments.callee);}test();
阶乘
var num = (function (n) { if(n == 1) { return 1; } return n * arguments.callee(n-1);}(100))
2、caller
es5会给函数对象上添加这个属性,基本浏览器早期版本都支持这个属性
引用当前函数被调用的环境,如果是全局作用域中调用则为null
function test () { demo();}function demo () { console.log(demo.caller);}test();
如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值
new.target
在函数里调用,如果该函数是使用new关键字调用的,则target会引用被调用的构造函数,如果该函数被当成普通函数运用,则返回undefined
function test() { if (new.target) { throw 'hello'; }}new test(); //hellotest(); //
属性
每个函数都有两个属性:length和prototype
length:该属性保存函数定义的命名参数的个数
function test(helo) {};test.length; //1
prototype:该属性不可枚举,所以使用for-in不会返回这个属性
方法
apply()、call()
这两个方法可以改变函数内this的引用,下文有详细说明
fun.apply(obj, arguments); //可以是Array实例也可以是arguments对象fun.call(obj, ...arguments); //参数必须一个个分开
在严格模式下,调用函数如果没有指定上下文对象,则this值不会指向window。除非使用apply()或call()把函数指定给一个对象,否则this值会变成undefined
函数表达式
任何时候,把函数当作值来用,他就是一个函数表达式
递归
阶乘
function mul(n){ if(n == 1){ return 1; } return n * mul(n-1);}
斐波那契数列
function fb(n){ if(n == 1 || n == 2){ return 1; } return fb(n-1) + fb(n-2);}
但是这样直接用函数名可能会有问题,因为如果这个变量被赋值为null则会报错,所以建议使用arguments.callee
但是在严格模式下,不能访问arguments.callee,所以这里可以用命名函数表达式
let f = function ff(num) { if (num <= 1) return 1; else return num * ff(num - 1);}f(3); //6
即使把函数值赋给另一个变量,函数表达式的名称ff也不变
尾调用优化
es6规定如果一个函数的返回时另一个函数的返回,则执行尾调用优化,具体如下
function outter() { return inner(); //尾调用}上述代码在es6中优化如下:1、执行到outter函数体,第一个栈帧被推到栈上2、执行outter函数体,到达return语句。为求值语句,必须先求值inner3、引擎发现把第一个栈弹出栈外也没关系,因为inner的返回值也是outter的返回值4、弹出outter的栈帧5、执行到inner函数体,栈帧被推到栈上6、执行inner函数体,计算其返回值7、将inner的栈帧推出到栈外
现在的浏览器没法测试尾调用优化是否起作用,但是这个是es6规范所规定的
尾调用优化条件
代码在严格模式下执行、外部函数的返回值时对尾调用函数的调用,尾调用函数返回后不需要执行额外的逻辑、尾调用函数不是引用外部函数作用域中自由变量的闭包
尾调用优化很适合递归,因为递归代码最容易在栈内存中产生大量栈帧
之所以要求严格模式,主要是因为非严格模式下函数调用允许使用f.arguments和f.caller,而它们都会引用外部函数的栈帧
立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)
定义后立即执行,执行完立即释放,不占用内存
(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笔试题
function test(a,b,c,d){ //...}(1,2,3);//这样子将不会报错//因为系统会认为你是function test(a,b,c,d){ //...}(1,2,3);//俩部分,前一部分是函数定义式,后一部分为表达式(‘,’也算是运算符)
在es5.1以前,为了防止定义的变量外泄,常常用IIFE,但是es6以后就可以用块级作用域了
//内嵌块级作用域{ 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()
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(){}123123f (){}
全局对象没有形参,所以没有第三步,而且第一步创建的是GO(Global Object)对象,
GO === window
console.log(a);var a = 123;//控制台输出:undefinedconsole.log(a);//控制台输出:error: a is not definedtest();function test(){ console.log("test");}//控制台输出:test
程序优先找自己所拥有的变量,在函数中优先AO中的对象,如果没有则向父级寻找,例如GO
**注意:**预编译不管有没有if ,只看声明
console.log(a); //undefinedconsole.log(c); //undefinedif(){ var a;//会被预编译 function c(){}//会被预编译}
最新:亲自实验IE、chrome、Edge,发现新特性!!!
if里面的function f函数声明会在GO和这个函数里面的AO同时声明f = function(),这是if语句里的特性
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);//consoleundefinedƒ 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]]就是我们所说的作用域,其中存储了执行期上下文对象的集合,这个集合呈链式链接,被称为作用域链。
作用域链是栈式结构
当函数执行时,会创建一个执行期上下文,多次创建会产生多个执行期上下文,每个都是独一无二的,函数结束时将会被会销毁。
function a(){ function b(){ var b = 234; } var a = 123; b();}var glob = 100;
b函数被创建时拿到的a函数的AO对象就是a函数的AO对象的引用
作用域链增强
执行上下文有全局上下文和函数上下文两种(eval()调用内部存在的第三种上下文),但有方式可以增强作用域链,即某些语句会在作用域前端临时添加一个上下文,这个上下文在代码执行后会被删除
有两种情况会出现:try-catch语句的catch、with
对于with来说是添加指定对象,对catch来说是创建一个新的变量对象,这个变量对象包含要抛出的错误对象的声明
with
with会将传入的参数当作with块里的作用域链的最近的AO(即作用域链顶端)
但是,正因为with能改变作用域链,这将会消耗大量资源,所以在es5.0中不允许被使用
var obj = { name : "obj"}var name = "window"function test() { var name = "scope"; with(obj) { consoel.log(name); // --> obj --> AO:test --> GO:window }}test();
with运用
//当我们要重复用某个对象上的方法时,可以利用withwith(document) { write('a'); //....}
如果with中使用var来定义变量,这个变量将会成为函数上下文中的一部分,而let则不会。
function test() { let hello = "j"; with(location) { var h = hello + href; } console.log(h);}test();//这里console.log能够访问得到h,但是with里换成let后就访问不到了。
垃圾回收
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会将创建的对象与隐藏类关联起来,以跟踪他们的属性,能够共享相同隐藏类的对象性能会更好,例如:
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操作会导致生成相同的隐藏类片段
function Art(str) { this.title = "same"; this.author = str;}let a1 = new Art();let a2 = new Art();delete a2.author;
这样操作会将a1和a2不再共享同一个隐藏类,最佳方案是将不想要的属性设置为null,这样既保持隐藏类不变和继续共享,也能达到删除引用值供垃圾回收程序回收的效果
内存泄漏:实际上是内存占用
意外声明全局变量
function test() { name = 'lsn';}
外部闭包很常见,如
let name = 'lsn';setInterval(() => { console.log(name);}, 100);// 只要定时器一直运行,回调函数中name就会一直占用内存,垃圾回收程序将不会清理name
内部闭包
let out = function() { let name = 'lsn'; return function() { return name; }}
静态分配与对象池
浏览器运行垃圾回收的一个标准是对象更替速度
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在哪里创建不会被垃圾回收调度程序盯上呢,答案是对象池
// 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中数组大小是动态可变的,所以注意不要招致额外的垃圾回收,需要在事先确定好这个数组大小。
闭包
闭包是指那些引用了另一个函数作用域中变量的函数
但凡是内部函数被保存到了外部必定会生成闭包,闭包会导致作用域链不释放,造成内存泄露
function a(){ function b(){ var bbb = 234; document.write(aaa); } var aaa = 123; return b;}var glob = 100;var demo = a();demo();
闭包的作用:
可以实现共有变量(函数累加器)、可以做缓存(储存结构)、可以实现封装,属性私有化、模块化开发,防止污染全局变量
闭包问题:
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
解决方法:
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
经典题型
var x = 1;if(function f() {}){ x += typeof f;}console.log(x);//输出“1undefined”//因为if的括号终究是括号,所以里面的函数定义式将会转化为表达式,函数等同于未声明,而且typeof函数是唯一一个不会报错的函数,f未定义,所以返回undefined,而且typeof返回的值是string型;所以和前面number:1相加就会变成字符串“1undefined”
对象Object
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 ++; }}
对象创建方法
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()方法,该方法接受三个参数
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()
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(),接收两个参数
let person = {};Object.defineProperties(person, { year: { value: 2017 }, year_: { get() { return this.year; } }})
读取属性的特性
Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符,接受两个参数(只对当前实例属性有效,如果想读取原型上的值,请直接对原型对象使用)
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()并在一个新对象上返回它们
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]]设置值
let dest, src, result;dest = {};src = {id: 'src'};result = Object.assign(dest, src);dest === result; //truedest !== 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) {...}}
如果多个源对象都有相同的属性,则使用最后一个复制的值,从源对象访问器属性取得的值,会作为一个静态值赋给目标对象,不能在两个对象间转移获取函数和设置函数
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(),该方法必须接受两个参数
true === 1; //false+0 === -0; //true+0 === 0; //true-0 === 0; //trueNaN === NaN; //falseisNaN(NaN); //trueObject.is(true, 1); //falseObject.is(+0, -0); //falseObject.is(+0, 0); //trueObject.is(-0, 0); //falseObject.is(NaN, NaN); //true
增强对象语法
属性值简写
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
可计算属性
不能在对象字面量中直接动态命名属性,要使用中括号[],而且中括号中的对象会被当作表达式求值
const name = 'lsn';let person = {};person[name] = 'matt';person.lsn; //mattfunction getName(name) { return `${name}`;}person = { [getName('lsn')]: 'matt'}person.lsn; //matt//可计算表达式中抛出任何错误都会中断对象的创建;计算属性的表达式抛出错误,之前完成的计算是不能回滚的
简写方法名
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
对象解构
使用与对象匹配的结构来实现对象属性的赋值
let person = { name: 'lsn', age: 19}let {name: personName, age: personAge} = person;personName; //lsnpersonAge; //19//简写let {name, age} = person;name; //lsnage; //19//如果不能匹配,则该变量的值就是undefinedlet {name, job} = person;name; //lsnjob; //undefined//可以在结构赋值的同时定义默认值let {name = 'h', job = 'h'} = person;name; //lsnjob; //h
结构函数在内部使用ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象,这意味着在对象解构的上下文中,原始值会被当成对象,所以null和undefined不能被解构,否则会报错
let {length} = 'foo';length; //3let {cunstructor: c} = 4;c === Number; //truelet {_} = null; //TypeErrorlet {_} = undefined; //TypeError
解构并不要求变量必须在解构表达式中声明,但是如果给事先声明的变量赋值,赋值表达式需要用括号括起来
let personName, personAge;let person = { name: 'lsn', age: 19}({name: personName, age: personAge} = person);
可以利用解构来复制对象属性
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//可以看作
在外层属性没有定义的情况下不能使用嵌套解构,无论源对象还是目标对象都一样
let personCopy = {};//foo在源对象上是undefined({foo: {title: personCopy.title}} = person);//报错//say在目标对象上是udefined({say: {h: person.say.h}} = person);//报错
如果解构中出错,则出错前赋值成功,出错后赋值失败,为undefined
函数参数中也可以使用解构赋值
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()
返回对象键值对数组,非字符串属性会被转为字符串输出,执行浅复制,忽略符号属性
冻结对象
将对象冻结,不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值,该对象的原型也不能被修改
虽然修改对象的属性不会报错(在严格模式下会报错),但是操作无效
let obj = {};Object.freeze(obj);obj.name = 'lsn';console.log(obj.name); //undefined
1、属性增加
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、属性修改
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并删掉
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、属性查询
mrDeng.sex; //点语法mrDeng["sex"];//输出male
创建对象-工厂模式
function creat(a, b) { let o = new Object(); o.a = a; o.b = b; return o;}
创建对象-构造函数模式
//要遵守大驼峰式命名规则,以区分普通函数和构造函数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");
构造函数内部原理
//当一个函数被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名
function test() {}new test();
构造函数里的方法会在每个实例上创建一遍
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,其值为构造函数。
Person.prototype.name = "hehe";Person.prototype.say = function () { console.log("hh");}//或者//但是下面这种方式会导致原型中没有constructorPerson.prototype = { name : "hehe", say : function () { console.log("hh"); }}function Person () {};var person = new Person();console.log(person);
原型增删改查,增加用xxx.prototype.xxx = xxx;
,删除用delete xxx.prototype.xxx
,查用xxx.xxx
就行,修改用xxx.prototype.xxx = xxx;
。
如果某个属性在当前对象未被定义时,会去原型prototype中查找。
除查询外,其他方法用xxx.xxx
将会是修改当前实体对象,并不是原型。
constructor可以手动修改
function Person() {};Car.prototype.constructor = Person;function Car() {};var car = new Car();
在之前讲过的new时三段构造里,this其实并不是空的,这样做是为了链接当前对象以及该对象的prototype,并且这种链接方式为引用。所以上方查询的描述才能实现。
//在new的时候,所进行的三步function Person() { //var this = { // __proto__ : Person.prototype //}; .......//某些赋值语句 //this = { // ......//赋值语句产生的属性 xxx : "xxx"/function () // __proto__ : Person.prototype //} //return this;}
下面代码出现的情况是因为__proto__
和prototype指向的空间已经不一样了
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的属性,指回与之关联的构造函数
Person.prototype.constructor === Person; //true
可以用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()解决这个问题,详情请看上文创建对象部分
原型的弊端
原型对象会共用同一个引用属性,所以一个实例所做的更改,另一个实例也会相查询到
原型链
可以将原型链比作作用域链
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
所有对象的最终原型是Object.prototype,Object.prototype的原型是null
对象修饰
原始值坚决不能有属性和方法,但是原始值如果以对象方法创建则可以加属性,同时还可以以原始值进行正常原始值的方法
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不可以有属性
上面说到原始值不能有属性和方法,但是
var str = "abcd";str.abc = 'a';//console// < str.abc// > undefined// < str.length// > 4//上面的原始值str可以访问length,但是不能创建abc对象
包装类
有 toString()
方法的包装类:Object、Number、Array、Boolean、String
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这个属性。当然,这俩对象也不会与原始值相同。
var num = 4;num.len = 3;//new Number(4).len = 3; => deleteconsole.log(num.len);//new Number(4).len//输出 undefinedvar str = "abcd";str.length = 2;//new String("abcd").length = 2; => deleteconsole.log(str.length);//new String("abcd").length//这是因为string对象里就有自带的length属性
Object构造函数作为一个工厂方法,会根据传入值的类型返回相应的原始值包装类型的实例
let obj = new Object("str");console.log(obj);console.log(typeof obj); //objectconsole.log(obj instanceof String); //true
而且要区分转型函数和构造函数
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()
第一个方法返回原始值,后面方法返回字符串值
原始值和实例化对象差异,不建议使用实例化对象
let value = false;let obj = new Boolean(false);let res = obj && true; //true,所有的对象在布尔表达式中都为true,因为Boolean(obj)返回trueres = value && true; //falseconsole.log(typeof value); //booleanconsole.log(typeof obj); //objectconsole.log(obj instanceof Boolean); //trueconsole.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():确定一个数是否为整数
Number.isInteger(1); //trueNumber.isInteger(1.00) //trueNumber.isInteger(1.01) //false
isSafeInteger():是否在安全数值内(IEEE 754整数范围内)
Number.MIN_SAFE_INTEGER (-2^53 + 1) ~ Number.MAX_SAFE_INTEGER (2^53 - 1)
Number.isSafeInteger(-1 * (2 ** 53)); //falseNumber.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):
接收任意数量的码元值创建字符串
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位码元的策略,也被称为代理对)
let message = "ab😊de";console.log(message.length); //6,因为实质上是返回字符串含有多少个16位码元,所以不是5,二是6,笑脸占两个16位码元console.log(message.charAt(1)); //bconsole.log(message.charAt(2)); //<?>console.log(message.charAt(3)); //<?>console.log(message.charAt(4)); //dconsole.log(message.charCodeAt(1)); //98console.log(message.charCodeAt(2)); //55357console.log(message.charCodeAt(3)); //56842console.log(message.charCodeAt(4)); //100console.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位
console.log(message.codePointAt(1)); //98console.log(message.codePointAt(2)); //128522console.log(message.codePointAt(3)); //56842,错误console.log(message.codePointAt(4)); //100//如果传入的码元索引并非代理对的开头,就会返回错误码点//迭代字符串可以正确的识别代理对的码点console.log([..."ab😊de"]); //["a", "b", "😊", "d", "e"]
fromCodePoint():
接收任意数量的码点创建字符串
let str = String.fromCodePoint(97, 98, 128552, 100, 101); //ab😊de###### normalize(xx):规范化字符为xx格式某些Unicode字符有多种编码方式```javascriptlet a1 = String.fromCharCode(0x00c5), a2 = String.fromCharCode(0x212B), a3 = String.fromCharCode(0x0041, 0x030A);console.log(a1); //Åconsole.log(a2); //Åconsole.log(a3); //Å//这三者字符看起来一摸一样,但是比较操作符只看码元是否一样,所以a1 != a2a2 != a3a1 != a3
为解决这个问题,Unicode提出四种规范化格式:NFD(Normalization From D)、NFC(Normalization From C)、NFKD(Normalization From KD)、NFKC(Normalization From KC)
//U+00C5是对0+212B进行NFC、NFKC规范化之后的结果console.log(a1 === a1.normalize("NFD")); //falseconsole.log(a1 === a1.normalize("NFC")); //trueconsole.log(a1 === a1.normalize("NFKD")); //falseconsole.log(a1 === a1.normalize("NFKC")); //true//U+212B是为规范化的console.log(a2 === a2.normalize("NFD")); //falseconsole.log(a2 === a2.normalize("NFC")); //falseconsole.log(a2 === a2.normalize("NFKD")); //falseconsole.log(a2 === a2.normalize("NFKC")); //false//U+0041/U+030A是0+212B进行NFD、NFKD规范化之后的结果console.log(a3 === a3.normalize("NFD")); //trueconsole.log(a3 === a3.normalize("NFC")); //falseconsole.log(a3 === a3.normalize("NFKD")); //trueconsole.log(a3 === a3.normalize("NFKC")); //false
选择同一种规范化格式能让比较操作符返回正确的结果
console.log(a1.normalize("NFD") === a2.normalize("NFD")); //trueconsole.log(a2.normalize("NFKC") === a3.normalize("NFKC")); //trueconsole.log(a1.normalize("NFC") === a3.normalize("NFC")); //true
concat():
将一个或多个字符串拼接成一个新字符串(不改变原字符串)
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从尾部开始搜
接收一个或两个参数,第一个参数表示要寻找的字符串,第二个参数表示从该位置开始搜
let str = "hello world";str.indexOf("o"); //4str.lastIndexOf("o"); //7str.indexOf("o", 6); //7str.lastIndexOf("o", 6); //4
startsWith()、includes()、endsWith():
可以传入一个或两个参数,第一个参数为要检查的字符串,第二个参数定义检查的起始位置,返回布尔值
startsWith()方法检测这个字符串是否为被检测字符串的开头(从0开始检查),如foobar
中,foo为开头,但是bar不是
includes()方法检测整个字符串(从0开始检查),只要是被检测字符串的子串就行
endsWith()方法检查字符串是否是被检测字符串的结尾(从结尾开始检查),如果加了第二个参数则默认字符串长度为第二个参数
let str = "foobarbaz";console.log(str.endsWith("barb", 6)); //falseconsole.log(str.endsWith("bar", 6)); //trueconsole.log(str.endsWith("baz", 6)); //false
trim():
删除字符串开头和结尾的所有空格,不改变原字符串
repeat():
接收一个整数参数,表示将字符串要重复多少次,然后将重复字符串拼接起来,不改变原字符串
padStart()、padEnd():
可以传入一个或两个参数,第一个参数表示复制的副本长度,第二个参数表示原字符串长度不足时的填充字符串,不改变原字符串
如果副本长度小于原字符串则返回原字符串
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"
迭代与解构
//字符串原型上暴露了@@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对象,第二个参数可以是一个函数或一个字符串,将匹配字符替换,不改变原字符串
涉及正则表达式后文有详解
第一个参数为字符串则只会替换第一个子串
let str = "abc dbc";str.replace("bc", "ood"); //"aood dbc"
split():
接收一个参数,该参数可以是字符串或RegExp对象,将匹配字符作为分隔符分割字符串
涉及正则表达式后文有详解
let str = "ab,c,d";str.split(","); //["ab", "c", "d"]
localeCompare():
按照字母表顺序比较两个字符串,字符串应该在字符串参数前则返回-1(通常为-1,具体要看与实际值相关的实现),如果相等则返回0,如果应该在后则返回1(通常为1,具体要看与实际值相关的实现)
let val = "abc";val.localeCompare("bcc"); //-1val.localeCompare("abc"); //0val.localeCompare("aab"); //1
有一点注意的是该方法会会根据实现所在的地区(国家和语言)决定比较顺序,不同国家不一样,例如在英国,该函数区分大小写,大写字母排在小写字母前,但在其他国家不一定如此
小题目:求字符串字节长度
var str = "fasdfasfasfhl;;laf";//当前字符位的unicode > 255,那么该字符串长度为2// <= 255 为1function 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.js
和illegal value.js
encodeURI不会编码属于URL组件的特殊字符,比如冒号、斜杠、问号、井号,而encodeURIComponent会编码它发现的所有非标准字符
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编码过的字符
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对象
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
自定义随机数函数
function selectFrom(x, y) { let range = y - x + 1; return Math.floor(Math.random() * range + 1);}selectForm(2, 10); //选择一个2-10的整数,包含2和10
还有很多方法,在这里就不多描述了,请查阅红宝书(p134)或上网寻找
JavaScript只处理16位有效数(小数点前后16位),不然会直接截断
call/apply
apply与call基本一样,俩者传参列表不同,apply只能传一个形参数组
call(obj, a, b); --> 一个个传形参apply(obj, [a, b]); --> 直接传arguments
改变this指向
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方法不传原函数形参
如果传参
应用
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
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不为直接引用,而是创建
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
我们需要在inherit函数里优化这个问题
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;}
例子
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、组合继承
实例不会共享原型非公共的引用值,但是可以共享公共的值(一般推荐)
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);
但是也会有弊端:第一是父类构造函数调用两次,第二是子类实例上有一组父类构造函数构造出来的属性,然而子类原型上也有
命名空间
为了防止团队开发时命名重复问题,老办法采取措施
var org = { a : { ff : 123; }, b : { ff : function () {} }}
也可以采用闭包
var init = (function () { var name = 'abc'; function callName() { console.log('name'); } return function () { callName(); }}())init();
模拟jq的连续执函数
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对象调用时,其实真正调用的是后者
obj.name --> obj['name']
对象枚举
遍历 枚举 enumeration
对象
var obj = { name : "name", sex : "sex", height : "height", __proto__ : { hh : "hh" }}
for in
for(var prop in obj) { console.log(prop, typeof(prop));}//key in obj//prop为对象属性名,会包括prototype//之后调用可以采用obj[prop]//不能采用obj.prop方式,因为obj会认为'.'后是属性字符串名,所以会转成obj['prop']
1、因为直接用for-in会将prototype也遍历到,所以我们用hasOwnProperty方法
for(var prop in obj) { if(obj.hasOwnProperty(prop)) { console.log(obj[prop]) }}//hasOwnProperty接触到含有__proto__为Object的原型链时会返回false//虽然我自己定义了一个—__proto__,但是这个__proto__里的__proto__是Objectobj = { __proto__ : { hh : "hh", __proto__ : Object }}
Object.prototype.abc = "deng";for(var prop in obj) { if(!obj.hasOwnProperty(prop)) { console.log(obj[prop]) }}
2、in
判断属性是否在对象里(包括原型),in前要加属性名,而且要是字符串,不然系统会认为是变量,返回Boolean型
> 'height' in obj< true> 'hh' in obj --> hh在__proto__里< true
this
对于一个函数的调用,谁调用了这个函数,这个函数里的this就指向谁
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:引用值会跟着某一个的改变而全部改变
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引进新的子页面,俩个页面之间的参数将会跨域
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构造函数创建,接收两个参数:目标对象和处理程序对象(缺一不可)
let target = { id: 'target'}const handler = {}const proxy = new Proxy(target, handler);target.id; //targetproxy.id; //targettarget.id = 'foo';target.id; //fooproxy.id; //foo//这个赋值会转移到目标对象proxy.id = 'bar';target.id; //barproxy.id; //bar//hasOwnProperty()在两个地方都会转移到目标对象target.hasOwnProperty('id'); //trueproxy.hasOwnProperty('id'); //true//Proxy.prototype是undefined,所以不能使用instanceof//严格相等可以用来区分代理和目标target === proxy; //false
定义捕获器
捕获器会在某些操作之前被调用并产生相应行为后继续返回这些操作,可以在处理程序对象中定义多个捕获器
捕获器其实是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流
const target = { id: 'lsn'}const handler = { //捕获器在程序中以方法名为键 //很多操作都会触发这个get捕获器 get() { return 'handler override'; }}let proxy = new Proxy(target, handler);target.id; //lsnproxy.id; //handler overridetarget['id']; //lsnproxy['id']; //handler overrideObject.create(target)['id']; //lsnObject.create(proxy)['id']; //handler override
捕获器参数和反射API
//get捕获器接收三个参数:目标对象,查询的属性,代理对象const handler = { get(trapTarget, property, receiver) { //trapTarget === target //property : foo //receiver === proxy }}proxy.foo;//通过手写代码如法炮制是不现实的,所以可以通过Reflect对象(封装了原始行为)的同名方法来轻松重建//处理程序对象中所有可以捕获的方法都有对应的反射APIconst 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; //barproxy.foo; //bar!!!
捕获器不变式
使用捕获器几乎可以改变所有基本方法行为,但也不是没有限制
每个捕获器方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵守“捕获器不变式”
捕获器不变式因方法不同而异,但通常会防止捕获器定义出现过于反常的行为
例如,目标对象有一个不可配置且不可写的数据属性,捕获器返回与该属性不同的值时会抛出异常
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异常
const {proxy, revoke} = Proxy.revocable(target, handler);proxy.foo; //barrevoke();proxy,foo; //TypeError
实用
反射API并不限于捕获程序
大多数反射API方法在Object类型上都有对应的方法
很多反射方法都返回”状态标记“的布尔值,表示该方法运行成功与否,这有时候比对象API返回修改后的对象或直接抛出异常要更有用
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);
代理一个代理
可以通过代理一个代理来构建一个多层拦截网
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指向调用函数的对象
let Foo = { say() { return this === proxy; }}let proxy = new Proxy(Foo, {});Foo.say(); //falseproxy.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()捕获器,所以可以根据所赋的值来决定允许还是拒绝
函数与构造函数参数验证
与保护和验证对象属性类似,也可对函数和构造函数参数进行审查,比如可以让函数只接受某种类型的值
数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起,这样可以实现各种模式,从而让不同的代码互相操作
类
虽然表面上看起来可以支持正式的面向对象编程,但是实际上它背后使用的任然是原型和构造函数的概念
定义类
//类声明class Foo{}//类表达式const Foo = class{};
函数声明可以提升,但类表达式不能,类受块作用域限制
构成
构造函数方法、实例方法、获取函数方法、设置函数方法、静态类方法
constructor() {}
、get f() {}
、static nn() {}
类表达式的名称是可选的,但是不能被类表达式作用域外部访问
let p = class Person{ some() {}}let f = new p();console.log(f); //Person{}console.log(p); //calss Person{some() {}}cossole.log(Person); //ReferenceError
类构造函数
与构造函数基本一致
//当一个类被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
class Person {}let p = new Person();console.log(p.constructor); //class Person {}let p1 = new p.constructor();console.log(p1); //Person{}
类其实就是一个特殊的函数
class Person{}console.log(typeof Person); //function
类标签符有prototype属性,这个原型也有一个constructor指向自身
class Person{}console.log(Person.prototype); //{constructor: f()}console.log(Person === Person.prototype.constructor); //true
类中定义的constructor不会被当成构造函数,但是直接将构造函数当普通构造函数构造的对象却不会
class Person{}let p = new Person();p.constructor === Person; //truep instanceof Person; //truep instanceof Person.constructor; //falselet p1 = new Person.constructor();p1.constructor === Person; //falsep1 instanceof Person; //falsep1 instanceof Person.constructor; //true
类可以像函数一样在任何地方定义,比如数组中,也可以像函数一样作为参数传参,也可以立即实例化
let arr = [class Person{}];function f(fun) { return new fun();}f(arr[0]);let p = new class Person{constructor(name){}}('lsn');
实例、原型和类成员
每个构造函数中的属性在实例上都是单独的,不会在原型上共享,但是在类块中定义的所有内容都会定义在类的原型上
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(); //hip.prototype.say(); //lll
类方法可以使用字符串、符号或计算的值作为键值
let symbolkey = new Symbol('symbolkey');class Person { say() {} [symbolkey]() {} ["a" + 1]() {}}
类定义也支持访问器属性,语法和普通函数一样
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引用类自身,其他所有约定和原型一样
class Person { static say() { console.log('say'); } static eat() { console.log('eat'); }}console.log(Person.say()); //sayconsole.log(Person.eat()); //eat//它还可以作为实例工厂class Person { static create() { return new Person(..); }}
类可以在定义外向类或类的原型添加成员数据
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就是实例本身
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 handsomeconsole.log(p);console.log(temp === p); //true
迭代器与生成器方法
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不仅可以继承一个类,也可以继承普通的构造函数
类和原型上定义的方法都会带到派生类
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可以调用父类构造函数
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也只能调用父级普通类方法
class Foo { static say() { console.log('lsn'); } sayName() { console.log('name'); }}class Bar { static say() { super.say(); } sayName() { super.sayName(); }}Bar.say(); //lsnnew Bar().sayName(); //name
es6给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象,这个指针是自动赋值的,super始终会定义为[[HomeObject]]的原型
注意:
super只能在派生类中使用
不能单独引用super,必须用它调用构造函数或者引用方法
调用this会调用父类构造函数并将返回值传给this
如果父类构造函数要传参数,则需要手动super(…, …)传参
如果没有定义类构造函数,实例化派生类时会调用super(),而且会传入所有传给派生类的参数如果在派生类中显式定义了一个构造函数,要么必须在其中调用this,要么返回一个对象
抽象基类
new.target保存通过new关键字调用的类或函数,通过实例化时检测new.target是不是抽象基类,可以阻止抽象基类实例化
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关键字来检查响应方法
class Foo { constructor() { if (!this.foo) { throw new Error('error'); } console.log('success'); }}class Bar extends Foo {foo() {}}class Van extends Foo {}new Bar(); //successnew Van(); //Error:error
继承内置类型
例如继承Array,定义自己的某些方法,但是产生的实例还是一个数组,可以调用Array的所有方法
有些内置类型的方法会返回新的实例,默认情况返回的新实例类型和原始实例类型是一样的,可以用覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类
class superArr extends Array {}let a = new superArr(1, 2, 3);let b = a.filter(x => !!(x%2))a instanceof superArr; //trueb instanceof superArr; //trueclass 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; //trueb instanceof superArr; //false
混入模式(很多js框架,特别是React抛弃了混入模式)
extends后面可以接一个表达式,表达式求值后必须为一个类或者构造函数
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(); //barb.baz(); //bazb.foo(); //foo//优化嵌套语句function mix(baseClass, ...mixins) { return mixins.reduce((pre, cur) => cur(pre), baseClass);}class Bar extends mix(Foo, BarMix, BazMix) {}
集合引用类型Object、Array
数组
数组的创建
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
数组除了引用它没有的方法,一般不会报错,例如
var arr = [];// > arr[10]// < undefined// > arr[10] = 10;// arr = [empty × 10, 10]
ES6引入两个新的创建数组静态方法:from()、of()
from():用于将类数组结构转换为数组实例
第一个参数接收一个类数组对象(任何可迭代的结构 或者 有一个length属性和可索引元素的结构)
//字符串会被拆分成字符串数组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)
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():返回索引/值对的迭代器
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(栈方法、队列方法)
//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//手写pushArray.prototype.push = function () { for(var i = 0; i < arguments.length; i++) { this[this.length] = arguments[i]; } return this.length;}
2、pop(栈方法、模拟反向队列)
//arr = [1, 2, 3]arr.pop();//arr = [1, 2]var num = arr.pop();//arr = [1]//num = 2,返回被删除的项
3、shift(队列方法)
//arr = [1, 2, 3];arr.shift();//arr = [2, 3];var num = arr.shift();//arr = [3]//num = 2,返回删除的项
4、unshift(模拟反向队列)
//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
//arr = [1, 2, 3]let arr1 = arr.reverse();//arr = [3, 2, 1]//arr1 = [3, 2, 1],返回调用它的数组的引用
6、splice
splice(切口下标,长度,往切口添加的数据…)
//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
//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
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+valuea.fill(8, -4, -1); //[0, 8, 8, 8, 0]//索引超出边界则无效,部分未超出边界部分有效//相反方向的索引无效,例如a.fill(8, 3, 0);
9、copyWithin
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影响,详情请查阅上文)
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方法,然后再拼接,以逗号分隔
//arr = [1, 2, 3]arr.toString();// "1, 2, 3"
3、slice
//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
//arr = [1, 2, 3, 4]var str = arr.join("-");//str = "1-2-3-4"join(); //按逗号连join(undefined); //按逗号连
4、split
//str = "1-2-3-4"var arr = str.split("-");//arr = [1, 2, 3, 4]
5、toLocaleString
调用每个值的toLoacleString方法,然后再拼接,以逗号分隔
//与toString方法没有什么不同,只是如果数组值上的toLocaleString被重写了的话就会不一样了
6、indexOf、lastIndexOf
传入一个或两个参数,第一个参数表示要查找的元素,第二个参数表示起始位置搜索位置,返回元素位置,使用全等(===)进行比较
前者方法从头开始,后者从尾开始
7、includes(es7)
传入一个元素,寻找在数组中是否包含此元素,返回布尔值,使用全等(===)进行比较
8、find、findIndex
这两个函数都使用了断言函数,接收一个或两个参数,第一个参数为断言函数,第二个参数为断言函数的this,从数组的最小索引开始,find返回匹配到的第一个元素,findIndex返回匹配到的第一个元素下标
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开始遍历整个数组
let values = [1, 2, 3, 4, 5];let sum = values.reduce((prev, cur, index, array) => {console.log(index); return prev + cur});//1 2 3 4console.log(sum);//15let values = [1, 2, 3];let sum = values.reduce((prev, cur, index, array) => {console.log(index); return prev + cur}, {});// 0 1 2console.log(sum);//[object Object]123
定型数组
因为WebGL的原因,催生出了Float32Array,是定型数组中可用的第一个”类型“
ArrayBuffer
ArrayBuffer是所有定型数组以及视图引用的基本单位,他是一个普通的js构造函数,可在内存中分配特定数量的字节空间
let buf = new ArrayBuffer(16);buf.byteLength; //16
ArrayBuffer一经创建就不能再调整大小了,但是可以用slice复制到新实例中
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,而且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置
const buf = ArrayBuffer(16);const fullDataView = new DataView(buf);fullDataView.byteOffset; //0fullDataView.byteLength; //16fullDataView.buffer === buf; //trueconst halfDataView = new DataView(buf, 0, 8); //byteOffset=0, byteLength=8halfDataView.byteOffset; //0halfDataView.byteLength; //8halfDataView.buffer === buf; //trueconst shalfDataView = new DataView(buf, 8); //byteOffset=8, byteLength=剩余长度shalfDataView.byteOffset; //8shalfDataView.byteLength; //8halfDataView.buffer === buf; //true
通过DataView读取缓冲,还需要几个组件:
读或写的字节偏移量,可以看成DataView中的某种“地址”
使用ElementType实现js的Number类型到缓冲内二进制格式的转换
内存中值得字节序,默认为大端字节序
ElementType
DataView对储存在缓冲中的数据类型没有预设,所以必须指定ElementType
es6支持八种ElementType
ElementType | 字节 | 说明 | 等价C类型 |
---|---|---|---|
Int8 | 1 | 8位有符号整数 | signed char |
Uint8 | 1 | 8位无符号整数 | unsigned char |
Int16 | 2 | 16位有符号整数 | short |
Uint16 | 2 | 16位无符号整数 | unsigned short |
Int32 | 4 | 32位有符号整数 | int |
Uint32 | 4 | 32位无符号整数 | unsigned int |
Float32 | 4 | 32位IEEE-754浮点数 | float |
Float64 | 8 | 64位IEEE-754浮点数 | double |
每种类型都有set和get方法,用byteOffset定位要读取或写入值得类型,类型可以是互换使用的
const buf = new ArrayBuffer(2);const view = new DataView(buf);//检查第一个和第二个字符view.getInt8(0); //0view.getInt8(1); //0//检查整个缓冲view.getInt16(0); //0//设置整个缓冲都为1//DataView会自动将数据转换为特定的ElementType//255的二进制表示:11111111(2^8 - 1)view.setUint8(0, 255);view.setUint8(0, 0xff);view.getInt16(0); //-1
字节序
”字节序“是计算机系统维护的一种字节顺序的约定,DataView只支持两种约定:大端字节序和小端字节序,大端字节序也称为“网络字节序”,意思是最高有效位保存在第一个字节,最低有效位保存在最后一个字节,小端字节序相反
一般来说js运行时所在系统的原生字节序决定了如何读取或写入字节,但是DataView并不遵守这个约定,DataView的所有API方法默认位大端字节序,但可以接受一个布尔参数,设置true可启用小端字节序
view.setUint8(0, 0x80);view.setUint8(1, 0x01);//0x8 0x0 0x0 0x1//1000 0000 0000 0001view.getUint16(0); //0x8001 32769view.getUint16(0, true); //0x0180 384view.setUint16(0, 0x0004);//0x0 0x0 0x0 0x4view.setUint16(0, 0x0004, true);//0x0 0x4 0x0 0x0
如果DataView读写操作没有足够的缓冲区就会报错,抛出RangeError
view.getInt16(1); //RangeErrorview.getInt16(-1); //RangeError
DataView会将写入缓冲区里的值尽量转换为适当的的类型,后备为0,如果无法转化,则抛出错误
view.setInt8(0, 1.5); //1view.setInt8(0, [4]); //4view.setInt8(0, 'f'); //0view.setInt8(0, Symbol()); //TypeError
定型数组
另一种形式的ArrayBuffer视图,他特定于一种ElementType且遵循系统原生的字节序。并且提供了更广的API和更高的性能,定型数组的目的就是为了提高与WebGL等原生库交换二进制数据的效率
const buf = new ArrayBuffer(12);const ints = new Int32Array(buf);ints.length; //3const ints1 = new Int32Array(6); //ArrayBuffer是24字节ints1.length; //6ints1.buffer.byteLength; //24const ints2 = new Int32Array([2, 4, 6, 8]);ints2.length; //4ints3.buffer.byteLength; //16ints2[2]; //6//复制ints2的值转换为Int16Arrayconst ints3 = new Int16Array(ints2);ints3.length; //4ints3.buffer.byteLength; //8ints2[2]; //6//通过普通数组创建const ints4 = new Int16Array.from([3, 4, 5, 6]);//通过参数创建const ints5 = new Int16Array.of(3, 4, 5, 6);
定型数组的构造函数都有一个BYTES_PER_ELEMENT属性,返回该类型数组中每个元素大小
Int16Array.BYTES_PER_ELEMENT; //2Int32Array.BYTES_PER_ELEMENT; //4ints1.BYTES_PER_ELEMENT; //4
定型数组默认以0填充缓冲
定型数组基本支持所有正常数组方法和属性,并且还有Symbol.iterator属性,可以用for-of迭代
但是因为定型数组不能修改缓冲区大小,所以对原数组有修改的函数不能使用,如:concat、push、pop
不过定型数组提供了两个方法,向内、向外复制数据:subarray()、set()
set
接收一个或两个参数,第一个参数为要复制的数组,第二个为复制到的起点索引(默认为0)
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相反的操作
a.subarray(2); //[3, 4, 5, 6, 7, 8]a.subarray(5, 7) //[6, 7]
上溢和下溢
某一个值的上溢和下溢不会影响到其他索引
类数组
类数组arguments没有数组的方法,但是与数组很相似,既能像数组一样用也能像对象一样用,但数组方法要自己添加
var obj = { "0" : 'a', "1" : 'b', "2" : 'c', "length" : 3, "push" : Array.prototype.push}//属性要为索引属性(数字)属性,必须要有length属性,最好加上pushvar 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: ƒ]
小题
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: ƒ}
数组去重
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
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); //truem3.get(undefined); //undefined
添加键值对
set方法会返回映射实例,所以可以连续使用 .
方法
m.set("first", "Matt") .set("second", "oh");
查询键值对
m.has("first");m.get("first");
删除键值对
m.delete("first"); //删除一个键值对m.clear(); //删除这个映射实例中所有键值对
Map内部可以使用任何js数据类型作为键,Map内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与Object类似,映射的值是没有限制的
如果键或值为引用值,只要指向的内存地址不变,键值对就能匹配到
但是也有奇怪的问题
const m = new Map();const a = 0/"", //NaN b = 0/"", //NaN pz = +0, nz = -0;a === b; //falsepz === nz; //truem.set(a, "foo");m.set(pz, "bar");m.get(b); //foom.get(nz); //bar
顺序与迭代
与Object类型不同的是,Map类型会维护键值对的插入顺序,所以可以根据插入顺序执行迭代操作
映射实例可以提供一个迭代器,能以插入顺序生成[key, value]形式的数据。通过entries()方法(或Symbol.iterator属性,它引用了entries())取得这个迭代器
const m = new Map([ ["key1", "val1"], ["key2", "val2"], ["key3", "val3"]]);m.entries === m[Symbol.iterator]; //truefor (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返回键值和值
Object与Map
内存占用上来说,不同浏览器内存分配实现不同,但是分配固定大小内存,Map大约可以比Object多储存50%的键值对
插入性能上来说,如果涉及大量插入,Map性能更佳
查找速度上来说,如果涉及大量查找操作,Object性能更佳
删除性能上来说,对大多数浏览器引擎来说,Map的delete操作都比插入和查找更快,如果涉及大量删除操作,Map性能更佳
WeakMap(es6)
弱映射是Map的兄弟类型,其API也是Map的子集,”Weak“代表js垃圾回收程序对待”弱映射“中键的方式
弱映射中的键只能是Object或继承自Object的类型,尝试使用非对象设置键会抛出TyprError异常
创建WeakMap
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中映射的键不属于正式的引用
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())
const s = new Set(["val1", "val2", "val3"]);s.values === s[Symbol.iterator]; //trues.keys === s[Symbol.iterator]; //truefor (let value of s.values()) { }for (let value of s[Symbol.iterator]()) { }
因为values()是默认迭代器,所以可以直接对集合实例使用扩展操作
[...s]; //["val1", "val2", "val3"]
entries()方法返回一个迭代器,可以按照插入的顺序返回包含两个元素的数组,是集合当前元素重复出现两边
for (let pair of s.entries()) { console.log(pair);}//["val1", "val1"]//["val2", "val2"]
同样的可以调用forEach方法,该方法接受两个参数,一是函数,二是this(改变函数this指向),第二个参数可不传
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迭代
这四种类型也都支持扩展操作符,扩展操作符在执行浅复制时特别有用,只需要简单的语法就能复制整个对象
let a1 = [1, 2, 3];let a2 = [...a1];a1 === a2; //false//也可以构建成数组的部分元素let a3 = [1, ...a1, 4];
对于期待可迭代的对象元素,只需要传入一个可迭代对象就能够实现复制
let m1 = new Map([[1, 2]]);let m2 = new Map(m1);m1; //1 -> 2m2; //1 -> 2
浅复制只能复制对象的引用
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作为键
这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器
//检测是否实现迭代器工厂函数let a = 1;console.log(a[Symbol.iterator]); //undefinedlet 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*操作符,在生成器中使用
这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器
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时,表示“耗尽”,即到达迭代对象尾端
迭代器维护者一个可迭代对象的引用,因此会阻止垃圾程序回收这个可迭代对象
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"}
显式迭代器和原生迭代器
//实现了可迭代接口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 {}
自定义迭代器
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引用的工厂函数会返回相同的迭代器
let arr = [1, 2];let iter1 = arr[Symbol.iterator]();let iter2 = iter1[Symbol.iterator]();iter1 === iter2; //true
所以也可以直接迭代迭代器
for (let i of arr) { console.log(i);}// 1 2for (let i of iter1) { console.log(iter1);}// 1 2
提前终止迭代器
可选的return()方法用于提前终止迭代器时执行的逻辑
可能的情况包括:for-of循环通过break、continue、return或throw提前退出
结构操作并未消费所有值
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 earlytry { for (let i of counter) { if (i > 2) { throw 'err'; } console.log(i); }} catch(e) { }//1 2 exiting earlylet [a, b] = counter;//exiting early
如果迭代器没有关闭,则还可以从上次离开的地方继续迭代(比如数组的迭代就是不可关闭的)
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 3for (let i of iter) { console.log(i);}//4 5
return方法是可选的,并非所有的迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象。但是仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的,因为调用return并不会强制迭代器进入关闭状态,但是return方法还是会调用
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 earlyfor (let i of iter) { console.log(i);}//4 5
生成器(es6)
生成器是es6中一个极为灵活的结构,拥有在一个函数块内暂停和回复代码执行的能力
生成器基础
生成器的形式是一个函数,函数名称前加一个星号(*)表示他是一个生成器,只要是可以定义函数的地方就可以定义生成器(除开箭头函数)
//生成器函数声明function* feneratorFn() {}//生成器函数表达式let generatorFn = function* () {}//作为对象字面量方法的生成器函数let foo = { * generatorFn() {}}//作为实例方法的生成器函数class foo { * generatorFn() {}}//作为静态方法的生成器函数class foo{ static * generatorFn() {}}
标识星号不受两侧空格影响
调用生成器函数会产生一个生成器对象,生成器对象一开始处于暂停执行(suspended)状态。生成器对象也实现了Iterator接口,具有next()方法,调用它会让生成器开始或恢复执行
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接口,它们默认的迭代器是自引用的
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()方法来恢复执行
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
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关键字只能在生成器函数的上下文使用,不然会报错
function* foo() { yield;}//truefunction* foo() { function bar() { yield; }}//false
生成器显式调用next()作用不大,一般把生成器当成可迭代对象使用起来会更方便
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()传入的值不会被使用,因为第一次调用是为了执行生成器函数
function* gener(init) { console.log(init); console.log(yield); console.log(yield);}let g = gener("foo");g.next("bar"); //foog.next("baz"); //bazg.next("quz"); //quz
yield关键字可以同时用于输入和输出
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,让其迭代一个可迭代对象
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,但对于生成器函数产生的迭代器来说,这个值就是生成器函数产生的值
function* gener() { console.log("iter", yield* [1, 2, 3]);}for (const x of gener()) { console.log(x);}//1 2 3 //iter undefinedfunction* 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
实现递归操作
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
生成器作为默认迭代器
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方法也可以,这两个方法都会强制让生成器进入关闭状态
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
let p = new Promise(() => {});setTimeout(console.log, 0, p); //Promise <pending>
期约状态机
期约有三种状态:待定(pending)、兑现(fulfilled,有时候也称为解决,resolved)、拒绝(rejected)
期约在待定状态下可以落定(settled)为代表成功的兑现状态和代表失败的拒绝状态,无论落定为哪种状态都是不可逆的,而且也不能保证期约必然会脱离待定状态,所以对于这三种状态都应该具有恰当的行为
期约状态是私有的,不能直接通过js检测到,而且不能被外部js代码修改
解决值、拒绝理由
每个期约只要状态切换为兑现就会产生一个私有的内部值;每个期约状态只要切换为拒绝就会产生一个私有的内部理由;无论是内部值还是内部理由,都是包含原始值或对象的不可修改引用
二者都是可选的,且默认值为undefined,在期约达到某个落定状态时执行的异步代码始终会收到这个值或理由
通过执行器函数控制期约状态
期约状态是私有的,所以只能通过内部进行操作
执行器函数有两项职责:初始化期约的异步行为和控制状态的最终转换
期约的最终状态转换是通过调用它两个函数参数实现的:
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()
通过这个实例化静态方法,可以定义一个解决的期约,而不是待定状态
//下面两个其实是一样的let p = new Promise((resolve, reject) => resolve());let p = Promise.resolve();//期约的解决值对应传给Promise.resolve()的第一个参数setTimeout(console.log, 0, Promise.resolve());//Promise <resolved>: undefinedsetTimeout(console.log, 0, Promise.resolve(1, 2));//Promise <resolved>: 1//多余的参数会被忽略
对于静态方法而言,如果传入的参数就是一个期约,那他的行为就类似于空包装,所以Promise.resolve()是一个幂等方法
let p = new Promise.resolve(3);p === Promise.resolve(p); //truep === Promise.resolve(Promise.resolve(p)); //true
这个幂等函数会保留期约的状态
let p = Promise(() => {});setTimeout(console.log, 0, p); //Promise <pending>setTimeout(console.log, 0, Promise.resolve(p)); //Promise <pending>
这个方法能够包装任何非期约的值,包括错误对象,并将其转换为解决的期约,所以可能导致不合法的行为
let p = Promise.resolve(new Error('foo'));setTimeout(console.log, 0, p);//Promise <resolved>: Error: foo
Promise.reject()
与Promise.resolve()类似,它会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能被try/catch捕获,只能通过拒绝处理程序捕获)
拒绝期约的理由就是传给Promise.reject()第一个参数,这个参数也会传给后续的拒绝处理程序
let p = new Promise.reject(3);setTimeout(console.log, 0, p);//Promise <reject>: 3p.then(null, (e) => setTimeout(console.log, 0, e)); //3
但是Promise.reject()没有等幂逻辑,如果转给他一个期约对象,这个期约对象会成为它拒绝的理由
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处理程序,两个参数表都是可选的,如果提供则会在期约分别进入 ”兑现“ 和 “拒绝” 状态时进行
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
p1.then('hello');//会被静默忽略,不推荐p2.then(null, () => onRejected('p2'));//推荐的写法
Promise.prototype.then()返回一个新的期约
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
let p1 = new Promise.resolve('foo');let p2 = p1.then();setTimeout(console.log, 0, p2); //Promise <resolved>: foolet p3 = p1.then(() => undefined);let p4 = p1.then(() => {});let p5 = p1.then(() => Promise.resolve());setTimeout(console.log, 0, p3); //Promise <resolved>: undefinedsetTimeout(console.log, 0, p4); //Promise <resolved>: undefinedsetTimeout(console.log, 0, p5); //Promise <resolved>: undefined
如果有显式的返回值,则Promise.resolve()会包装这个值
let p6 = p1.then(() => 'bar');let p7 = p1.then(() => Promise.resolve('bar'));setTimeout(console.log, 0, p6); //Promise <resolved>: barsetTimeout(console.log, 0, p7); //Promise <resolved>: bar//保留返回的期约let p8 = p1.then(() => new Promise(() => {}));let p9 = p1.then(() => Promise.reject());//Uncaught (in Promise): undefinedsetTimeout(console.log, 0, p8); //Promise <pending>setTimeout(console.log, 0, p9); //Promise <rejected>: undefined//抛出异常会返回拒绝的期约let p10 = p1.then(() => {throw 'baz'});//Uncaught (in Promise): bazsetTimeout(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处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约
let p = new Promise.resolve('foo');let p1 = p.finally(() => new Promise(() => {}));let p2 = p.finally(() => Promise.reject());//Uncaught (in promise) undefinedsetTimeout(console.log, 0, p1); //Promise <pending>setTimeout(console.log, 0, p2); //Promise <rejected>: undefinedlet p3 = p.finally(() => {throw 'bar';});//Uncaught (in promise) barsetTimeout(console.log, 0, p3); //Promise <rejected>: bar
如果返回待定期约,则期约一解决,新期约还是会原样后传初始期约
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运行时保证,被称为”非重入“特性
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添加的处理程序都是如此
p.finally(() => setTimeout(console.log, 0, '1'));p.finally(() => setTimeout(console.log, 0, '2'));//1//2
传递解决值和拒绝理由
再执行函数中,解决值和拒绝理由分别是作为resolve()和reject()的第一个参数往后传递的。然后,这些值又会传给它们各自的处理程序,作为onResolved或onRejected处理程序的唯一参数
let p = new Promise((res, rej) => res('foo'));p.then((value) => console.log(value)); //foo
拒绝期约与拒绝错误处理
拒绝期约类似于throw()表达式,因为他们都代表一种程序状态,即需要中断或者特殊处理,在期约的处理程序或执行函数中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由
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,但是建议统一使用错误对象,因为错误对象可以让浏览器捕获错误对象中的栈追踪信息,例如:
Uncaught (in promise) Error: foo at Promise (test.html:5) at new Promise (<anonymous>) at test.html:5
因为期约中抛出错误时,错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令
异步错误只能通过异步的onRejected处理程序捕获
这不包括执行函数中的错误,再拒绝或解决期约前,处理函数中仍然可以通过try/catch来捕获错误
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,都是捕获错误后将其隔离,同时不影响正常代码运行
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())都会返回新的期约对象,而这个期约又有自己的实例方法,这样连缀方法调用就可以构成自己的”期约连锁“
期约图
期约连锁可以构建有向非循环图结构
例如二叉树:
// a// b c//d e f glet 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.rece()
Promise提供的两个将多个期约实例组合成一个期约的静态方法
合成后的期约的行为取决于内部期约的行为
Promise.all()
这个方法创建的期约会在一组期约全部解决后再解决,这个静态方法接受一个可迭代对象,返回一个新期约
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
合成的期约只会在每个包含的期约完成之后才解决,如果其中有一个期约待定,则合成的期约待定;如果其中有一个期约拒绝,则合成的期约也会拒绝;只有所有的期约都解决,合成的期约才会解决,并且解决值就是所有包含期约的解决值的数组,按照迭代器的顺序:
let p = Promise.all([ Promise.resolve(3), Promise.resolve(), Promise.resolve(4)])p.then((val) => setTimeout(console.log, 0, val)); //[3, undefined, 4]
如果有期约拒绝,则第一个拒绝的期约将会称为合成的期约的拒绝的理由,之后再拒绝的期约的理由不会再影响合成期约的拒绝的理由。但是,这不会影响所有的包含期约正常的拒绝操作
//第一个期约的拒绝理由会进入合成期约的拒绝处理程序,但是第二个期约也会静默处理,不会有错误跑掉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()
这个方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像,该方法接收一个可迭代对象
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()一样,这并不影响所有的包含期约正常的拒绝操作,其他包含的期约的拒绝操作会被静默处理
串行期约合成
可以将多个函数作为处理程序合成一个连续传值的期约连锁
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规范却未涉及的两个特性:期约取消和进度追踪
期约取消
可以在现有基础性上进行封装,可以实现取消期约的功能
下面是一个计时器例子:
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()方法
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
用于声明异步函数,它可用于函数声明、函数表达式、箭头函数和方法上
async function foo() {}let bar = async function() {};let baz = async () => {};class Quz { async quz() {}}
使用该关键字可以让函数具有异步特征,但是总体上其代码仍然是同步求值的。在参数或闭包方面,异步函数仍然具有普通函数的正常行为
async function foo() { console.log(1);}foo();console.log(2);//1//2//foo函数仍然会在后面的指令之前运行
不过,异步函数如果使用了return关键字返回了值(没有return则返回undefined),这个值会被Promise.resolve()包装成一个期约对象,异步函数始终返回期约对象
async function foo() { console.log(1); return 3; //直接返回一个期约也行}foo().then(console.log);console.log(2);//1//2//3
其实异步函数期待返回一个实现thenable接口的对象(实际并不是),常规值也行
如果是实现了thenable对象,则这个对象可以由提供给then()的处理程序”解包“;如果不是,则返回值就被当做已经解决的期约
async function baz() { const thenable = { then(callback) {callback('baz');} }; return thenable;}baz().then(console.log);//bazasync function quz() { return Promise.resolve('qux');}quz().then(console.log);//qux
于期约处理程序一样,在异步函数中抛出错误会返回拒绝的期约
async function foo() { console.log(1); throw 3;}foo().catch(console.log);console.log(2);//1//2//3
不过拒绝期约的错误不会被异步函数捕获
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关键字可以暂停异步函数代码的执行,等待期约解决
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一元操作一样,可以单独使用,也可以在表达式中使用:
async function foo() { console.log(await Promise.resolve('foo'));}foo();//fooasync function foo() { return await Promise.resolve('foo');}foo().then(console.log);//fooasync function foo() { await new Promise((resolve, reject) => setTimeout(resolve, 1000)); console.log('foo');}foo();//foo 1 second after
await关键字期待一个thenable接口的对象(但实际上并不要求),常规值也可以
如果是实现了thenable对象,则这个对象可以由await来”解包“;如果不是,则这个值就被当做已经解决的期约
async function foo() { console.log(await 'foo');}foo();//fooasync function foo() { const thenable = { then(callback) {callback('foo');} }; console.log(await thenable);}foo().then(console.log);
await等待会抛出错误的同步操作,会返回拒绝的期约
async function foo() { console.log(1); await (() => {throw 3;})();}foo().catch(console.log);console.log(2);//1//2//3
对拒绝的期约使用await则会释放(unwrap)错误值(将拒绝期约返回):
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关键字后面跟着一个立即可用的值,函数其余部分也会被异步求值
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函数的方法
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
利用平行执行
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
这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成;这样可以保证顺序执行,但是总执行事件会被变长
如果不在意执行顺序,可以一次性初始化所有期约
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简化
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
栈追踪于内存管理
期约与异步函数的功能有相当程度的重叠,但他们在内存中的表示则差别很大
看看拒绝期约的栈追踪信息:
function fooPromiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'bar');}function foo() { new Promise(fooPromiseExecutor);}foo();//Uncaught (in Promise) bar//setTimeout (async)//fooPromiseExecutor//foo
按理来说这些函数已经返回了,因此不应该看到它们
其实这是因为js引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时,调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中,这也意味着栈追踪信息会占用内存,从而带来一些计算和储存成本
如果使用异步函数:
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月)、日(131)、时(023)、分、秒
年和月必须传,其他不传默认最小值
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执行 !!
当interval里的function用字符串代替也行,比如 setInterval("console.log('a')", 1000)
这样也会执行在控台打印a
var timer = setTimeout(function, time)
timer是一个数值作为这个timeout的唯一标识
timeout里function中的this指向window(除箭头函数外)
这里还有一种用法,就是可以传更多参数给setTimeout,然后这些参数会传入setTimeout的第一个参数函数中
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严格模式方法
在代码逻辑的最上方写
"use strict";//全局的最顶端,将会在全局启用function test() { "use strict"; //局部的最顶端,将会在局部启用,不影响全局}
使用了es5.0严格模式后,将会有以下方法限制
1、arguments.callee
2、func.caller
3、with(obj) {}
4、变量赋值前必须声明
5、局部的this必须被赋值,赋值什么就是什么
6、不重复的属性和参数
7、eval()
能改变作用域
//es3.0 都不能用eval(); eval是魔鬼var a = "123";eval('console.log(a)');var global = 100;function test() { global = 200; eval('console.log(global)');}test();// 200function test() { var global = 200; eval('global = 100;console.log(global);'); console.log(global);}// 100// 100
正则表达式 RegExp
匹配特殊字符或有特殊搭配原则的字符的最佳选择
转义字符 \
行结束 \r
换行 \n
Table \t
多行字符串代码
1、``
var test = `fadsfaasdfadfasfasdf`
2、转义字符
var test = "fahskjdfhasf\发生放大是发\dasfaf"
创建正则表达式
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
使用正则表达式
str.match(reg); //返回匹配的片段数组reg.test(str); //返回true或falsereg.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里会将子表达式添加进类数组里
match不启用全局匹配的话和上述一样
"aabb".match(/(\w)\1(\w)\2/);
加上全局匹配的话
元字符
. === [^\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字符
量词
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个aa+? 则只取一个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 | 正则表达式的标记字符串 |
var reg = /^\s/g;reg.ignoreCase;-> falsereg.source -> "^\s"reg.flags -> "g"
RegExp 实例方法
方法 | 描述 |
---|---|
compile | 编译正则表达式 |
exec | 检索字符串中指定的值。返回找到的值,并确定其位置 |
test | 检索字符串中指定的值。返回 true 或 false |
toString | 返回正则表达式的字面量字符串,如“/abc/g” |
toLocaleString | 与toString方法一样 |
valueOf | 返回正则表达式本身(不是字符串) |
/^a/g.exec("abc");->["a", index: 0, input: "abc", groups: undefined]0: "a"groups: undefinedindex: 0input: "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/yreg.exec(str); //index:0, "cat", lastIndex:3reg.exec(str); //null, lastIndex:0//因为以索引为3的字符开头找不到匹配项(,被强制作为第一个字符),lastIndex被设置为0reg.lastIndex = 5;reg.exec(str); //index:5, "bat", lastIndex:8let reg = /abc/g;console.log(reg.toString());console.log(typeof reg.toString());console.log(reg.valueOf());console.log(typeof reg.valueOf());->/abc/gstring/abc/gobject
RegExp 构造函数属性
全名 | 简写 | 说明 |
---|---|---|
input | $_ | 最后搜索的字符串 |
lastMatch | $& | 最后匹配的文本 |
lastParen | $+ | 最后匹配的捕获数组 |
leftContext | $` | input字符串中出现在lastMatch前面的文本 |
rightContext | $’ | input字符串中出现在lastMatch后面的文本 |
这些属性可以提取出exec()和test()执行操作的相关信息
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()时,这些属性会被填充
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:按照匹配到的字符串拆分,但是会将子表达式添加到类数组中
replace:未写正则表达式全局的话只匹配一次,replace(reg, str);
!!!在str里可以用$1$2
,来引用reg里的子表达式
//如果要把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一样
正向预查(正向断言)、非正向预查
var str = "abaaaaaa";var reg = /a(?=b)/g; 匹配a后面紧接b的那个astr.match(reg);->["a"]var reg = /a(?!b)/g;str.match(reg);->["a", "a", "a", "a", "a", "a"]
练习
//the-first-name//theFirstNamevar 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, '.');