基本语法
[0]. JavaScript原生提供的三个包装对象
- Number
- String
- Boolean
Number与parseInt
parseInt:字符串转为整数,字符依次转换,遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分
Number:将任意类型的值转化成数值,字符串整体转换,若不可以被解析为数值,返回NaN
/// parseInt/Number的返回值只有两种可能,一个十进制整数或NaN parseInt('8.8a') //8 Number('8.8a') //NaN parseInt(''); //NaN parseInt(null); //NaN parseInt(undefined); //NaN Number(''); //0 Number(null); //0 Number(undefined); //NaN
Number要比parseInt严格很多。基本上只要有一个字符无法转成数值,整个字符串就会被转为NaN。
- 第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。
- 第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。
- 第三步,如果toString方法返回的是对象,就报错。
var obj = {x: 1}; Number(obj); // NaN // 等同于 if (typeof obj.valueOf() === 'object') { Number(obj.toString()); } else { Number(obj.valueOf()); } Number({ valueOf: function () { return 2; }, toString: function () { return 3; } }); // 2
表示valueOf方法先于toString方法执行。
注:一元运算符(+)同Number()方法功能一样。
String
将任意类型的值转化成字符串。
String(true) // "true" String(undefined) // "undefined" String(null) // "null"
与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。
- 先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
- 如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
- 如果valueOf方法返回的也是对象,就报错。
String({ valueOf: function () { return 2; }, toString: function () { return 3; } }); // "3"
表示toString方法先于valueOf方法执行。
字符串方法总结
split() 方法用于把一个字符串分割成字符串数组。 slice方法:slice(start,end) end是可选参数。1个参数:从开始位置截取到结尾 ,2个参数:从开始位置截取到结束位置(不包括结束位置本身,结束位置还可以是负数) push方法 :可向数组的末尾添加一个或多个元素,并返回新的长度。 pop方法:用于弹出数组最后一个元素,返回该元素的值 shift:将数组第一个元素从其中删除,并且返回第一个元素的值 unshift: 在数组开头添加元素,返回该元素值 join:将数组元素通过指定的字符连接成字符串。参数若是为无,默认为',' reverse:用于将数组元素颠倒顺序。无参数,操作的是数组本身,会改变原数组 sort(fn):将数组元素排序,会改变原数组的。 参数:fn(a,b):比较函数 ;无参数的时候按照字母表升顺排序;参数a,b分别表示数组中的任意两个元素,若 return > 0 则表示 则b前a后,若 return < 0 则表示 a前b后。 splice:用于截取数据,插入数据,删除数据。操作的是数据本身。 参数:1.开始位置 2.截断的个数 3.插入的数据 indexOf:返回在该数组中第一个找到的元素位置,若不存在则返回 -1 filter:筛选函数,不改变原数组。 ----------------------------------------------------------------------------- 传统上,JavaScript只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。 includes():返回布尔值,表示是否找到了参数字符串。 startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。 endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。 repeat():用于返回一个新字符串,表示将原字符串重复n次。 eg:'x'.repeat(3) //"xxx" 参数如果是小数,会被取整。 参数如果是负数或Infinity,会报错。 如果参数是0到-1之间的小数,则等同于0。 如果repeat的参数是字符串,则会先转换成数字。 参数NaN等同于0。 padStart(),padEnd():如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。 padStart和padEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。 如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。 如果省略第二个参数,则会用空格补全长度。 模板字符串:ES6引入了模板字符串解决这个问题。 eg: $('#result').append(` There are <b>${basket.count}</b> items in your basket, <em>${basket.onSale}</em> are on sale! `); 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。 模板字符串中嵌入变量,需要将变量名写在${}之中。
Boolean
将任意类型的值转为布尔值。
Boolean(false) // false Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true
[1]. JavaScript中三种方法确定值的类型:
- typeof运算符
- instanceof运算符
- Object.prototype.toString方法
typeof(null) --> "object" typeof(undefined) --> "undefined" typeof(NaN) --> "Number"
非数值类型转换为数值类型
6 + null --> 6 // Number(null); 6 + undefined --> NaN // Number(undefined);
instanceof
表示对象是否为某个构造函数的实例。但,只能用于对象,不适用原始类型的值。
检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。
v instanceof V // 等同于 V.prototype.isPrototypeOf(v)
利用该运算符,可以巧妙解决忘记new命令的问题
function GG (g1, g2) { if (this instanceof GG) { this._g1 = g1; this._g2 = g2; console.log("111111"); } else { console.log("222222"); return new GG(foo, bar); } } var GG2 = new GG(1,2); // 111111 var GG1 = GG(1,2); // 222222 // 111111
字节序
分为小端(little endian)和大端(big endian):
- 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法
- 小端字节序:低位字节在前,高位字节在后
下面是判断计算机字节序的方法
// true:小端,false:大端 var littleEndian = (function() { var buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true); // 小端字节序写入 return (new Int16Array(buffer))[0] === 256; })();
通常个人PC都是小端字节序,因为计算机先处理低位字节,效率比较高。
以32位整数求值为例
/* 大端字节序 */ i = (data[3]<<0) | (data[2]<<8) | (data[1]<<16) | (data[0]<<24); /* 小端字节序 */ i = (data[0]<<0) | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);
关于字节序,参见:理解字节序 - 阮一峰;
[2]. Infinity
Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)
Infinity与NaN比较,总是返回false(任何值(包括NaN本身)与NaN比较,返回的都是false)
0 * Infinity // NaN Infinity - Infinity // NaN Infinity / Infinity // NaN
Infinity与null运算,等同于与0运算
Infinity与undefined运算,总是等于NaN
[3]. 布尔类型
以下6个的布尔值为false,其余均为true
undefined null false 0 NaN ""或''(空字符串)
空数组[]和空对象{}也为true。
! 与 !!:强制转换为布尔型
- !:类型判断,逻辑取反,可将null、undefined和空串取反为false
- !!:类型判断,双重逻辑取反,判断变量 !!a 等效于 a!=null&&typeof(a)!=undefined&&a!=''&&a!=""
[4]. 字符集
JavaScript使用Unicode字符集。
每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。
也就是说,JavaScript的单位字符长度固定为16位长度,即2个字节。
JavaScript无法识别四字节的字符,也就是说,JavaScript返回的字符串长度可能是不正确的。
判断一个字符由两个字节还是由四个字节组成的最简单方法:
function is4Bytes(c) { return c.codePointAt(0) > 0xFFFF; }
[5]. Base64编码
- 将文本中不可打印的特殊符号转成可打印的字符;
- 以文本格式传递二进制数据;
非ASCII码字符转的Base64编码,需要转码环节
function b64Encode(str) { return btoa(encodeURIComponent(str)); } function b64Decode(str) { return decodeURIComponent(atob(str)); } b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE" b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
借此,了解下 js中 encodeURI和 encodeURIComponent
- 把字符串作为 URI 进行编码
- 方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
主要的区别如下
- encodeURI(URIstr): 对在 URI 中具有特殊含义的 ASCII 标点符号,不会进行转义的:;/?:@&=+$,#
- encodeURIComponent(URIstr):对在 URI 中具有特殊含义的 ASCII 标点符号,也会进行转义的:;/?:@&=+$,#
[6]. isNaN() 与 isFinite()
isNaN():判断一个值是否为NaN,利用NaN是唯一不等于自身的值,用于isNaN()判断
isFinite():表示某个值是否为正常的数值,Infinity、-Infinity、NaN和undefined返回false,其余均返回true
[7]. 数组
数组的length属性的值 = 最大的数字键 + 1
将数组的键分别设为字符串或小数(为数组添加属性),结果都不影响length属性。
- push和pop:“后进先出”的栈结构(stack)
- push和shift:“先进先出”的队列结构(queue)
二进制数组
允许以二进制数组操作二进制数据,支持浏览器与显卡之间以二进制的形式进行通信。
- ArrayBuffer对象:代表原始的二进制数据。表示内存之中的一段二进制数据,支持以数组的方法操作内存
- TypedArray对象:代表确定类型的二进制数据。用来生成内存的视图
- DataView对象:代表不确定类型的二进制数据。用来生成内存的视图,支持自定义格式和字节序
对ArrayBuffer的读写均需通过视图实现。
TypedArray视图和DataView视图,两者的区别主要是字节序,前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。
二进制数组应用
- Ajax
- Canvas
- WebSocket
- Fetch API
- File API
[8]. 闭包
JavaScript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。
也就是说,父对象的所有变量,对子对象都是可见的,反之则不成立。
闭包就是函数,能够读取其他函数内部变量的函数。
由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包最大的特点:“记住”诞生的环境
闭包的最大用处有两个:
- 可以读取函数内部的变量
- 让变量始终保持在内存中,即闭包使诞生环境一直存在(闭包可以看作是函数内部作用域的一个接口)
本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁
实际中,常用于封装对象的私有属性和私有方法
[9]. eval命令
将字符串当作语句执行:
- 直接使用:eval(expression),当前局部作用域
- 间接调用:除eval(expression)外,全局作用域
var a = 1; function f() { var a = 2; var foo = eval; foo("console.log(a)"); } f() // 1 var a = 1; function f() { var a = 2; eval("console.log(a)"); } f() // 2
该命令JavaScript引擎不优化,运行速度较慢。
通常情况下,eval最常见的场景是解析Json数据字符串。
最正确的做法应该是:使用浏览器提供的JSON.parse方法
[10.1]. 对象
对象转换成原始类型的值:obj.valueOf().toString()
- 对象的valueOf方法总是返回对象自身
- 对象的toString方法默认返回[object Object]
JavaScript语言的继承是通过“原型对象”(prototype),而不是“类”(class)。
至于原因,参见:Javascript继承机制的设计思想;
注:在出现对象或函数的地方,可以用类替换,或许更容易理解。
new
C#/Java/C++中的new用法
类名/var obj = new 类名();
而JS中的new用法,略有不同,是其简化设计版
var obj = new 构造函数名();
使用new命令时,其后面的构造函数依次执行:
- 创建一个空对象,作为将要返回的对象实例
- 将这个空对象的原型,指向构造函数的
prototype
属性 - 将这个空对象赋值给函数内部的
this
关键字 - 开始执行构造函数内部的代码
特别地,注意构造函数体中有return语句的情况。
new命令简化的内部流程:
// /* 构造函数,构造函数参数 */ function _new( constructor, params) { // 将 arguments 对象转为数组 var args = [].slice.call(arguments); // 取出构造函数 var constructor = args.shift(); // 创建一个空对象,继承构造函数的 prototype 属性 var context = Object.create(constructor.prototype); // 执行构造函数 var result = constructor.apply(context, args); // 如果返回结果是对象,就直接返回,否则返回 context 对象 return (typeof result === 'object' && result != null) ? result : context; }
toString()
Object.prototype.toString方法返回对象的类型字符串,可以判断一个值的类型:Object.prototype.toString.call(value)
var type = function (o){ var s = Object.prototype.toString.call(o); return s.match(/[object (.*?)]/)[1].toLowerCase(); };
这是比 typeof 运算符更准确的类型判断函数。
this
this是属性或方法“当前”所在的对象。但是,this的指向是动态的。
- 全局环境:this指向顶层对象window(this === window; //true)
- 构造函数:指向实例对象
- 对象的方法:指向方法运行时所在的对象
var obj ={ foo: function () { console.log(this); } }; // 表示调用foo方法,this指向对象obj obj.foo() // 表示属性foo对应的值,this指向顶层对象 obj.foo
直白理解,JS引擎内部,obj和obj.foo储存在两个内存地址:地址一和地址二。obj.foo()这样调用,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,obj.foo表示直接取出地址二执行,因此运行环境就是全局环境,this指向全局环境。
注意:
- 避免多层this
- 避免在数组处理方法中使用this
- 避免在回调函数中用this
对于多层this,可以将外层的this先赋值给临时变量tmp,内层函数调用this的地方用tmp替换。
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { var that = this; this.p.forEach(function (item) { console.log(that.v+' '+item); }); } }
this绑定
JS提供 call
、apply
、bind
三种方法,实现动态切换/固定this
的指向。
(1)call
指定函数内部this的指向。默认参数为空,则指向全局对象。
func.call(thisValue, arg1, arg2, ...)
(2)apply
同call方法,唯一的区别就是,它接收一个数组作为函数执行时的参数
func.apply(thisValue, [arg1, arg2, ...])
- 将数组的空元素变为undefined
- 转换类数组对象为真正的数组
- 绑定回调函数的对象
call和apply方法绑定函数执行时所在的对象,还会立即执行函数。
(3)bind
将函数体内的this绑定到某个对象,然后返回一个新函数,而且(每一次绑定均返回一个新的函数)
注:涉及到this时(特别是有些库方法内部有this,不易察觉),在将函数赋值给其他变量时,务必注意this的绑定。
对this的详细理解,请参见:this关键字;
prototype
JS规定
每一个构造函数都有一个prototype属性,该属性指向另一个特殊的对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
JS继承机制的设计思想:原型对象的所有属性和方法都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象均可共享。
- 数据共享,节省内存
- 体现实例对象之间的联系
可以理解成接口或者是类的静态变量。实例对象可以视作是从原型对象衍生出来的子对象。
JavaScript规定:
- 每个函数都有一个
prototype
属性,指向一个对象 - 所有对象都有自己的原型对象prototype
原型链
prototype chain:对象到原型,再到原型的原型,直至null。
Object.prototype
是所有对象的原型,而Object.prototype
的原型是null
。
Object.getPrototypeOf(Object.prototype); // null
prototype
对象的constructor
属性,默认指向prototype
对象所在的构造函数 。
function F() {} var f = new F(); f.constructor === F // true f.constructor === f.prototype.constructor // true
利用该属性,可以判定某个实例对象,是哪个构造函数产生的。
f.constructor.name; // "F"
也可以由一个实例对象新建另一个实例,利用f.constructor
间接调用构造函数
F.prototype.createCopy = function () { return new this.constructor(); };
constructor
属性表示原型对象与构造函数之间的关联关系,若修改原型对象,需同时修改constructor
属性的指向。
// 好的写法:将constructor属性重新指向原来的构造函数 F.prototype = { constructor: F, method1: function (...) { ... }, // ... }; // 更好的写法:只在原型对象上添加方法 F.prototype.method1 = function (...) { ... };
Object
获取原型对象的标准方法:Object.getPrototypeOf
// 实例对象f的原型是 F.prototype Object.getPrototypeOf(f); // F.prototype // 空对象的原型是 Object.prototype Object.getPrototypeOf({}); // Object.prototype // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype); // null // 函数的原型是 Function.prototype function f() {} Object.getPrototypeOf(f); // Function.prototype
实例对象的__proto__
属性,返回该对象的原型,等同于getPrototypeOf
方法。
生成实例对象的常用方法:Object.create
实质是新建一个空的构造函数F
,然后让F.prototype
属性指向参数对象obj
,最后返回一个F
的实例,从而实现让该实例继承obj
的属性。
Object.create = function (obj) { function F() {} F.prototype = obj; return new F(); };
创建一个空对象
var obj1 = Object.create({}); var obj2 = Object.create(Object.prototype); var obj3 = new Object();
若想生成一个纯粹的空对象,即不继承任何属性
var obj = Object.create(null);
获取所有属性键名:Object.getOwnPropertyNames
只返回对象本身的,不包含继承的,而且不管是否可以遍历(与Object.keys
的区别)。
对应的,hasOwnProperty
方法用于判定是否存在于对象本身(与in的区别),该方法是JS中唯一一个处理对象属性时,不会遍历原型链的方法。
获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举)
function allPropertyNames(obj) { var props = {}; while(obj) { Object.getOwnPropertyNames(obj).forEach(function(p) { props[p] = true; }); obj = Object.getPrototypeOf(obj); } return Object.getOwnPropertyNames(props); }
对象的拷贝
确保拷贝后的对象:
- 与原对象具有同样的原型
- 与原对象具有同样的实例属性
function copyObject(orig) { // 保证原型相同 var copy = Object.create(Object.getPrototypeOf(orig)); // 保证实例属性相同 copyOwnPropertiesFrom(copy, orig); return copy; } function copyOwnPropertiesFrom(target, source) { Object .getOwnPropertyNames(source) .forEach(function (propKey) { var desc = Object.getOwnPropertyDescriptor(source, propKey); Object.defineProperty(target, propKey, desc); }); return target; }
采用ES2017引入的标准的Object.getOwnPropertyDescriptors
方法,简化为
function copyObject(orig) { return Object.create( Object.getPrototypeOf(orig), Object.getOwnPropertyDescriptors(orig) ); }
关于拷贝,可参见:Javascript面向对象编程(三):非构造函数的继承 中 深拷贝 部分,此处仅引用其代码
function deepCopy(p, c) { var c = c || {}; for (var i in p) { if (typeof p[i] === 'object') { c[i] = (p[i].constructor === Array) ? [] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; }
其实,此代码仅满足了上述的第2个条件。
属性描述对象
attributes object,JS提供的内部数据结构,用来描述对象的属性,控制对象的行为
- value
- writable
- enumerable
- configurable
- get
- set
如果一个属性的enumerable为false,下面三个操作不会取到该属性
for..in
循环Object.keys
方法JSON.stringify
方法
存取器:get 与 set ,有2种定义形式(第2种更常用)
// 第一种 var obj = Object.defineProperty({}, 'p', { get: function () { return 'getter'; }, set: function (value) { console.log('setter: ' + value); } }); // 第二种,常用 var obj = { get p() { return 'getter'; }, set p(value) { console.log('setter: ' + value); } };
拷贝:将一个对象的所有属性,拷贝到另一个对象
var copyF= function (to, from) { for (var property in from) { if (!from.hasOwnProperty(property)) continue; Object.defineProperty( to, property, Object.getOwnPropertyDescriptor(from, property) ); } return to; }
行为控制:控制读写状态,防止对象被改变
Object.preventExtensions:无法添加新属性
Object.seal:既无法添加新属性,也无法删除旧属性
Object.freeze:无法添加新属性、无法删除旧属性、也无法改变属性的值,近似常量
从上至下,控制性越强。Object.seal实质是把属性描述对象的configurable属性设为false。
若想彻底锁定对象,需要锁定对象的原型
var obj = new Object(); Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); Object.preventExtensions(proto);
这样就避免通过改变原型对象,间接改变对象属性。
[10.2]. 继承
子构造函数(子类)继承父构造函数(父类)
- 子类继承父类的实例:在子类的构造函数中,调用父类的构造函数
- 子类继承父类原型:子类的原型指向父类的原型
function Son() { Father.call(this); } Son.prototype = Object.create(Father.prototype); Son.prototype.constructor = Son;
关于继承的问题,参见:Javascript面向对象编程(二):构造函数的继承;
另一种实现形式,异曲同工
function extend(Son,Father){ var Tmp = function(){}; Tmp.prototype = Father.prototype; var tmpObj = new Tmp( ); Son.prototype = tmpObj; Son.prototype.constructor = Son; //必写,否则孩子的构造函数指向不对 Son.super = Father.prototype; //可写,提供孩子访问父亲的方法 }
该方法也是 YUI 库实现继承的方法,图示原型链
特别是在:原型继承,对原型链的图示,有助于对prototype的理解。
JS不提供多重继承功能(不允许一个对象同时继承多个对象),但可以间接实现
function M1() { this.hello = 'hello'; } function M2() { this.world = 'world'; } function S() { M1.call(this); M2.call(this); } // 继承 M1 S.prototype = Object.create(M1.prototype); // 继承链上加入 M2 Object.assign(S.prototype, M2.prototype); // 指定构造函数 S.prototype.constructor = S;
该实现模式称为 Mixin(混入)。
封装私有变量
- 普通构造函数形式
- 立即执行函数(IIFE):不暴露私有成员
- 模块的放大模式:支持模块拆分和继承
- 宽放大模式:与放大模式相比,宽放大模式的“立即执行函数”的参数可以是空对象
// 放大模式 var module2 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1); // 宽放大模式 var module2 = ( function (mod){ //... return mod; })(window.module1 || {});
在未出现class关键字前,可采用如下方式模拟类
var Animal = { name: "Animal", createNew: function(){ var animal = {}; animal.sleep = function(){ alert(name + "sleep"); }; return animal; } }; var Cat = { createNew: function(_name){ var cat = Animal.createNew(); cat.name = _name; cat.makeSound = function(){ alert(_name + "miao"); }; return cat; } };
在ES6中,引入了class关键字,更接近Java/C#的用法,极大地简化了原型链代码。
[11]. == 与 ===
- ==:相等运算符,比较两个值是否相等
- ===:严格相等运算符,比较它们是否为“同一个值”
如果两个值不是同一类型,===直接返回false,而==会将它们转换成同一个类型,再用===进行比较。
对于两个对象的比较,===比较的是地址,而大于或小于运算符比较的是值。
// undefined和null与自身严格相等 undefined ===/== undefined //true null ===/== null //true NaN ===/== NaN //false undefined == null //true
不要使用相等运算符(==),最好只使用严格相等运算符(===)。
[12]. 位运算
所有的位运算都只对整数有效。
位否运算:一个数与自身的取反值相加,等于-1。
位否运算遇到小数时,会将小数部分舍去,只保留整数部分。对一个小数连续进行两次二进制否运算,能达到取整效果。
~~ -1.9999 // -1
异或运算也可以实现取整运算
-8.9 ^ 0 //-8
但是使用二进制否运算取整,是所有取整方法中最快的一种。
位或运算:将任意数值转为32位带符号整数
function toInt32(x) { return x | 0; } toInt32(Math.pow(2, 32) + 1) // 1 toInt32(Math.pow(2, 32) - 1) // -1
[13]. switch…case
通常是 case...break 的形式
function doAction(action) { switch (action) { case 'hack': return 'hackAction'; break; case 'slash': return 'slashAction'; break; case 'run': return 'runAction'; break; default: throw new Error('Invalid action.'); } }
建议写成对象结构,避免代码冗长
function doAction(action) { var actions = { 'hack': function () { return 'hackAction'; }, 'slash': function () { return 'slashAction'; }, 'run': function () { return 'runAction'; } }; if (typeof actions[action] !== 'function') { throw new Error('Invalid action.'); } return actions[action](); }
[14]. JSON
stringify()与parse()
- JSON.stringify:将一个值转为JSON字符串
- JSON.parse:将JSON字符串转换成对应的值
toJSON()
若参数对象有自定义的toJSON方法,那么JSON.stringify会使用此方法的返回值作为参数,而忽略原对象的其他属性。
var user = { firstName: '三', lastName: '张', get fullName(){ return this.lastName + this.firstName; }, toJSON: function () { return { name: this.lastName + this.firstName }; } }; JSON.stringify(user); // "{"name":"张三"}" /// 等效于 JSON.stringify(user.toJSON());
toJSON()的一个应用是,将正则对象自动转为字符串。
因为JSON.stringify默认不能转换正则对象,但设置了toJSON()后,就可以转换正则对象
var obj = { reg: /foo/ }; // 不设置 toJSON 方法时 JSON.stringify(obj) // "{"reg":{}}" // 设置 toJSON 方法时 RegExp.prototype.toJSON = RegExp.prototype.toString; JSON.stringify(/foo/) // ""/foo/""
[15]. 正则表达式
正则表达式中,需要反斜杠转义的一共有12个字符:^、.、[、$、(、)、|、*、+、?、{和\。
- [Ss]:指代一切字符
- w:匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
replace
如果不加g修饰符,仅替换第一个匹配成功的值,否则替换所有匹配成功的值。
'aaa'.replace('a', 'b') // "baa" 'aaa'.replace(/a/, 'b') // "baa" 'aaa'.replace(/a/g, 'b') // "bbb"
replace方法的一个应用:去除空格
// str不变 var str = ' #id div.class '; var ss = str.replace(/^s+|s+$/g, ''); 或 var ss = str.trim();
量词符
用来设定某个模式出现的次数。
- ? 问号表示某个模式出现0次或1次,等同于{0,1}
- * 星号表示某个模式出现0次或多次,等同于{0,}
- + 加号表示某个模式出现1次或多次,等同于{1,}
默认最大可能匹配:贪婪模式
若改为非贪婪模式,在 * 或 + 的后面加上 ? 即可。
修饰符
g 表示全局匹配(global),主要用于搜索和替换 i 表示忽略大小写(ignorecase) m 表示多行模式(multiline),使^和$会识别换行符( ) s 使得.可以匹配任意单个字符,包括换行符 或 (dotAll模式) y “粘连”(sticky)修饰符,g 的加强版 u 用于正确处理大于uFFFF的 Unicode 字符(Unicode 模式)
断言
- 先行断言:lookahead,
/x(?=y)/
- 先行否定断言:negative lookahead,
/x(!=y)/
- 后行断言:lookbehind,/(?<=y)x/
- 后行否定断言:negative lookbehind,
/(?<!y)x/
关于正则表达式,详情请见:http://javascript.ruanyifeng.com/stdlib/regexp.html
具名组匹配
利用exec提取()匹配的结果,同时为每一个结果预先指定名字。
- 定义:?<name>
- 使用:res.groups.name
也可以在其他地方引用:$<name>
若是在某个正则表达式内部引用:k<name>
[16]. 计时方法
计时
获取某段代码的执行耗时
console.time('计时器名'); xxx...xxx console.timeEnd('计时器名'); // 计时器名: xxx.xxxxms
定时器
setTimeout()
setInterval()
运行机制:将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间
- 如果不到,继续等待
- 如果到了,且本轮事件循环的所有同步任务执行完,再执行对应的代码(回调函数)
为确保两次执行之间有固定的间隔,不建议用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间
var timer = setTimeout(function f() { // ... timer = setTimeout(f, 2000); }, 2000);
上面代码可以确保,下一次执行总是在本次执行结束之后的2000毫秒开始。
有关setTimeout()的原理,请参见:https://www.jianshu.com/p/3e482748369d?from=groupmessage
防抖动:debounce()
假定两次Ajax通信的间隔不得小于2500毫秒,实际中应该
$('textarea').on('keydown', debounce(ajaxAction, 2500)); function debounce(fn, delay){ var timer = null; // 声明计时器 return function() { var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }
若是如下格式,会存在连续点击触发keydown事件、频繁调用回调函数的问题。
$('textarea').on('keydown', ajaxAction);
setTimeout(fun, 0)
尽可能早地执行fun,但是并不能保证立刻就执行fun。
- 调整任务执行顺序
- 拆分计算量大耗时长的任务:代码块高亮、改变网页背景色
关于定时操作,可参见:定时器 - 阮一峰;
[17]. 异步操作
JS异步操作的几种模式
- 回调函数:最基本
- 事件监听:事件驱动
- 发布/订阅:观察者模式
其中,发布订阅模式优于事件监听模式,具体参考:异步操作概述 - 阮一峰;
Promise
Promise对象是JavaScript的异步操作解决方案,为异步操作提供统一接口
- 代理作用(proxy),充当异步操作与回调函数之间的中介桥梁
- 使得异步流程可以写成同步流程
设计思想:所有异步任务都返回一个Promise实例。
- 预加载图片
- Ajax操作
Promise支持:串行执行异步任务 和 并行执行异步任务。
关于Promise,可参见:Promise对象 - 阮一峰;Promise - 廖雪峰;
[18]. 有限状态机
Finite-state machine
- 状态(state)总数是有限的
- 任一时刻,只处在一种状态之中
- 某种条件下,会从一种状态转变(transition)到另一种状态
结合异步操作,与对象的状态改变挂钩,当异步操作结束时,发生相应的状态改变,由此再触发其他操作。
可以利用有限状态机的函数库:Javascript Finite State Machine
var fsm = StateMachine.create();
- 允许为每个事件指定两个回调函数:onBeforeEventA,onAfterEventA
- 允许为每个状态指定两个回调函数:onLeaveState1,onEnterState1
若事件EventA使得由状态State1变为State2,则调用顺序为
onBeforeEventA → onLeaveState1 → onEnterState2 → onAfterEventA
参见:JS与有限状态机;Javascript State Machine;
DOM模型
JS执行速度远高于DOM。
详情请参见:JS - DOM - sqh;
Ajax
详情请参见:JS - Ajax - sqh;
参考: