JavaScript

基本语法

[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中 encodeURIencodeURIComponent

  • 把字符串作为 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提供 callapplybind 三种方法,实现动态切换/固定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;
 }
};

参见:Javascript定义类(class)的三种方法

在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个字符:^、.、[、$、(、)、|、*、+、?、{和\\。

  • [\S\s]:指代一切字符
  • \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),使^和$会识别换行符(\n)
s 使得.可以匹配任意单个字符,包括换行符\r或\n (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;  

参考

JavaScript 标准参考教程(alpha)- 阮一峰ECMAScript 6  - 阮一峰

JavaScript教程 - 廖雪峰

posted @ 2018-06-13 21:10  万箭穿心,习惯就好。  阅读(540)  评论(1编辑  收藏  举报