《JavaScript语言入门教程》记录整理:运算符、语法和标准库

目录

本系列基于阮一峰老师的《JavaScrip语言入门教程》或《JavaScript教程》记录整理,教程采用知识共享 署名-相同方式共享 3.0协议。这几乎是学习js最好的教程之一(去掉之一都不过分)

最好的教程而阮一峰老师又采用开源方式共享出来,之所以重新记录一遍,一是强迫自己重新认真读一遍学一遍;二是对其中知识点有个自己的记录,加深自己的理解;三是感谢这么好的教程,希望更多人阅读了解

运算符

算数运算符

  1. js提供了10种运算符
  • 加法运算符:x + y
  • 减法运算符:x - y
  • 乘法运算符:x * y
  • 除法运算符:x / y
  • 指数运算符:x ** y
  • 余数运算符:x % y
  • 自增运算符:++x 或者 x++
  • 自减运算符:--x 或者 x--
  • 数值运算符: +x
  • 负数值运算符:-x
  1. js中非数值可以相加,比如布尔值与数值相加,字符串相加用于连接两个字符串
true + true // 2
1 + true // 2

1 + \'a\' // "1a"
false + \'a\' // "falsea"

加法运算符是在运行时决定,到底是执行相加,还是执行连接。运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)

\'3\' + 4 + 5 // "345"
3 + 4 + \'5\' // "75"

加法运算符存在重载。减法、除法和乘法等运算符不会重载:所有运算子一律转为数值,再进行相应的数学运算

  1. 对象的相加:运算子是对象时,会先转成原始类型的值,然后再相加。

对象默认转成原始类型的值是[object Object]

var obj = { p: 1 };
obj+5    // "[object Object]5"

对象转成原始类型的值,规则:

  • 自动调用对象的valueOf方法。对象的valueOf方法默认返回对象自身
  • 再调用toString方法转为字符串。对象的toString方法默认返回[object Object]

自定义valueOf方法或toString方法(同时改写两个方法时要小心),改变对象相加的结果

obj.valueOf()   // {p: 1}
obj.valueOf().toString()    // "[object Object]"

obj.valueOf=function () {
    return 1;
  }
obj+5  // 6

唯一的特例是,当运算子是Date对象时,会优先执行toString方法

var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return \'hello\' };

obj + 5 // "hello5"
  1. 余数运算符(%)返回前一个运算子被后一个运算子除所得的余数。结果的正负号由第一个运算子决定
-1 % 2 // -1
1 % -2 // 1

可以使用绝对值,获得负数的正确余数值

// 正确的写法
function isOdd(n) {
  return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
  1. 自增和自减运算符是一元运算符,只有一个运算子。

运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。

自增/自减放在变量后面,会先返回变量操作前的值,再进行自增/自减操作

自增/自减放在变量之前,会先进行自增/自减操作,再返回变量操作后的值

  1. 数值运算符(+)的作用可以将任何值转为数值(与Number函数作用相同)

负数值运算符(-),将一个值转为数值的负值

不会改变原始变量的值,而是返回新值

  1. 指数运算符(**)完成指数运算

指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2  // 512
  1. 赋值运算符(Assignment Operators)用于给变量赋值。还有复合的赋值运算符,如x += yx -= y

比较运算符

  1. 比较运算符比较两个值的大小,并返回一个布尔值。js提供了8个比较运算符
  • > 大于运算符
  • < 小于运算符
  • <= 小于或等于运算符
  • >= 大于或等于运算符
  • == 相等运算符
  • === 严格相等运算符
  • != 不相等运算符
  • !== 严格不相等运算符
  1. 相等比较和非相等比较。

对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。

  1. 相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较两个值是否为“同一个值”。

如果两个值不是同一类型,严格相等运算符===直接返回false,而相等运算符==会将它们转换成同一个类型,再进行比较

  1. 严格相等运算符:类型不同返回false;同一类型的原始类型值,会比较两者的值是否相等;复合类型的值(对象、数组、函数)比较的是是否指向同一个地址;undefined和null与自身严格相等

两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值

var obj1 = {};
var obj2 = {};

obj1 > obj2 //  比较的是值 false
obj1 < obj2 //  比较的是值 false
obj1 === obj2 // 比较的是地址 false

相等运算符比较是隐含了类型转换,建议最好只使用严格相等运算符(===)。

布尔运算符

  1. 布尔运算符用于将表达式转为布尔值。一共有4个
  • 取反运算符:!
  • 且运算符:&&
  • 或运算符:||
  • 三元运算符:?:
  1. 取反运算符将布尔值变为相反值。两次取反就是将一个值转为布尔值的简便写法
  2. 且运算符&&常用于多个表达式的求值

且运算符&&运算规则是:如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。

&&且运算可以用来取代if语句

if (i) {
  doSomething();
}

// 等价于

i && doSomething();
  1. 或运算符(||)也用于多个表达式的求值。

或运算符||的运算规则是:如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。

或运算符常用于为一个变量设置默认值。

function saveText(text) {
  text = text || \'\';
  // ...
}

// 或者写成
saveText(this.text || \'\')
  1. 且运算符和或运算符,这种通过第一个表达式(运算子)的值,控制是否运行第二个表达式(运算子)的机制,就称为“短路”(short-cut)
  2. 三元条件运算符(?:)是js中唯一一个需要三个运算子的运算符

二进制位运算符

  1. 二进制位运算符用于直接对二进制位进行计算,一共有7个:
  • 二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1。
  • 二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0。
  • 二进制否运算符(not):符号为~,表示对一个二进制位取反。
  • 异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0。
  • 左移运算符(left shift):符号为<<
  • 右移运算符(right shift):符号为>>
  • 头部补零的右移运算符(zero filled right shift):符号为>>>
  1. 位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。虽然在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数

利用这个特性,可以写出一个函数,将任意数值转为32位整数。

function toInt32(x) {
  return x | 0;
}
  1. 位运算符可以用作设置对象属性的开关。(开关作用有些抽象,但很精巧)

假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。A、B、C、D四个开关,每个开关占有一个二进制位

var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
  • 用二进制与运算,检查当前设置是否打开了指定开关
var flags = 5; // 二进制的0101
// 检验是否打开了开关C
if (flags & FLAG_C) {  // 0101 & 0100 => 0100 => true
  // ...
}
  • 假设需要打开ABD三个开关,可以先构造一个掩码变量,然后通过二进制或运算掩码变量,可以确保打开这三个开关
var mask = FLAG_A | FLAG_B | FLAG_D;
// 0001 | 0010 | 1000 => 1011

flags = flags | mask; // 代表三个开关的二进制位都打开的变量
  • 二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭
flags = flags & mask;
  • 异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值。
flags = flags ^ mask;
  • 二进制否运算可以翻转当前设置
flags = ~flags;

void和逗号运算符

  1. void运算符,执行一个表达式,然后不返回任何值,或者返回undefined
void 0 // undefined
void(0) // undefined   推荐写法

void运算符的优先级很高,使用括号避免错误

var x = 3;
void (x = 5) //undefined
x // 5
  1. void运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超链接中插入代码防止网页跳转。

如下代码,点击链接后先执行onclick,然后返回false,所以浏览器不会跳转

<script>
function f() {
  console.log(\'Hello World\');
}
</script>
<a href="http://example.com" onclick="f(); return false;">点击</a>

void运算符可以取代上面的写法:

<a href="javascript: void(f())">文字</a>

或者,实现点击链接提交表单,但不产生页面跳转

<a href="javascript: void(document.form.submit())">
  提交
</a>
  1. 逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
\'a\', \'b\' // "b"

var x = 0;
var y = (x++, 10);
x // 1
y // 10

用途是:在返回一个值之前,进行一些辅助操作。

var value = (console.log(\'Hi!\'), true);
// Hi!

value // true

运算顺序

  1. 运算符优先级别(Operator Precedence)高的先执行
  2. 圆括号()用来提高运算的优先级(它的优先级最高),即圆括号中的表达式会第一个运算

圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。

函数放在圆括号中,会返回函数本身。圆括号紧跟在函数的后面,表示调用函数。

圆括号之中,只能放置表达式

  1. "左结合"(left-to-right associativity)运算符会先从左向右运算

    "右结合"(right-to-left associativity)运算符会先从右向左运算

js中赋值运算符(=)、三元条件运算符(?:)、指数运算符(**)是"右结合"的

语法

数据类型的转换

  1. JavaScript 是一种动态类型语言,变量的类型无法在编译阶段确定,必须在运行时才能知道。而同时js的变量类型又可以随意改变,因此又属于弱类型语言
  2. JS中的运算符对数据类型有要求。因此常常发生类型自动转换
  3. 强制类型转换主要指使用Number()String()Boolean()手动将任意类型的值,分别转换成数字、字符串或者布尔值。
  4. Number()转换为数值。比parseInt函数严格
  • 转换原始类型的值
// 数值:转换后还是原来的值
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number(\'324\') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number(\'324abc\') // NaN

// 空字符串转为0
Number(\'\') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

// 忽略前后空格
Number(\'\t\v\r12.34\n\') // 12.34
  • 转换对象时,规则如下:

    第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。

    第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。

    第三步,如果toString方法返回的是对象,就报错。

自定义valueOftoString

Number({
  valueOf: function () {
    return 2;
  }
})
// 2

Number({
  toString: function () {
    return 3;
  }
})
// 3

Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// 2
  1. String()转换字符串的规则如下:
  • 原始类型的值

    数值:相应的字符串。

    字符串:原来的值。

    布尔值:true-"true",false-"false"。

    undefined:"undefined"。

    null:"null"。

  • 对象

    String参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

转换规则如下:

第一步,先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

第二步,如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。

第三步,如果valueOf方法返回的是对象,就报错。

String({
  toString: function () {
    return 3;
  }
})
// "3"

String({
  valueOf: function () {
    return 2;
  }
})
// "[object Object]"

String({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
})
// "3"
  1. Boolean()转换为布尔值,规则简单,除了下面6个值结果为false,其余全部为true

undefinednull0(包含-0+0)、NaN\'\'(空字符串)和false

所有对象(包括空对象)的转换结果都是true,包括false对应的布尔对象new Boolean(false)也是true

  1. js中数据类型自动转换发生的情况:一、不同类型的数据相互运算时会自动转换。二、对非布尔值类型的数据求布尔值时。三、对非数值类型的值使用一元运算符(即+-)。转换时的规则是:预期什么类型的值,就调用该类型的转换函数。如果该位置既可以是字符串,又可以是数值,则默认转为数值
  2. JavaScript在预期为布尔值的地方(比如if语句的条件部分),会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。

如下两个方法将一个表达式转为布尔值

// 写法一
expression ? true : false

// 写法二
!! expression
  1. 除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

null数值0undefined数值NaN

错误处理机制

  1. JavaScript原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。当发生错误时,js引擎抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。
var err = new Error(\'出错了\');
err.message // "出错了"
  1. Error实例的属性:
  • message:错误提示信息
  • name:错误名称(非标准属性)
  • stack:错误的堆栈(非标准属性)
function throwit() {
  throw new Error(\'\');
}

function catchit() {
  try {
    throwit();
  } catch(e) {
    console.log(e.stack); // print stack trace
  }
}

catchit()
// Error
//    at throwit (<anonymous>:2:9)
//    at catchit (<anonymous>:7:5)
//    at <anonymous>:1:1
  1. Error实例是最一般的错误类型,js还提供Error的6个派生对象
  • SyntaxError对象:解析代码时发生的语法错误
  • ReferenceError对象:引用一个不存在的变量时发生的错误。
  • RangeError对象:一个值超出有效范围时发生的错误。
  • TypeError对象:变量或参数不是预期类型时发生的错误。
  • URIError对象:URI相关函数的参数不正确时抛出的错误。主要encodeURI()decodeURI()encodeURIComponent()decodeURIComponent()escape()unescape()
  • EvalError对象:已不再使用
  1. 自定义错误
function UserError(message) {
  this.message = message || \'默认信息\';
  this.name = \'UserError\';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
  1. throw语句:手动中断程序执行,抛出一个错误。
if (true) {
  throw new Error(\'x 必须为正数\');
}
// Uncaught Error: x 必须为正数
//    at <anonymous>:2:9

throw可以抛出任何类型的值

  1. try...catch结构用于对错误进行处理,选择是否往下执行。catch代码块捕获错误后,程序不会中断。
try {
  throw new Error(\'出错了!\');
} catch (e) {
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出错了!
//   at <anonymous>:3:9
//   ...

catch代码块中加入判断语句,捕获不同类型的错误

try {
  foo.bar();
} catch (e) {
  if (e instanceof SyntaxError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}
  1. try...catch...finally结构中的finally代码块,不管是否出现错误,都会在最后执行。
function cleansUp() {
  try {
    throw new Error(\'出错了……\');
    console.log(\'此行不会执行\');
  } finally {
    console.log(\'完成清理工作\');
  }
}

finally代码块前面即使有return返回语句,依旧会执行完再返回。

function idle(x) {
  try {
    console.log(x);
    return \'result\';
  } finally {
    console.log(\'FINALLY\');
  }
}

idle(\'hello\')
// hello
// FINALLY

如下说明:return语句的执行在finally代码之前,只是等到finally执行完最终才返回

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}

countUp()
// 0
count
// 1

finally代码块的典型场景

openFile();

try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}

编程风格

  1. "编程风格"(programming style)指的是编写代码的样式规则。

    你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格

  2. 编程风格主要考虑的几点:缩进(indent)、区块(block)、圆括号(parentheses)、行尾的分号、变量声明、严格相等、语句的合并书写等
  3. 使用{}代码块时,js中要使用左大括号{紧挨着语句在同一行中,不要换行写。这是因为JavaScript会自动添加句末的分号,从而产生一些难以察觉的错误。
block {
  // ...
}

如下return语句其实会变成两句,从而导致出问题

return
{
  key: value
};

// 相当于
return;
{
  key: value
};

// 正确写法
return {
  key : value
};
  1. 行尾的分号:分号表示一条语句的结束。js允许省略。

有三种情况,语法规定不需要在结尾添加分号。如果添加,js引擎将分号解释为空语句

    1. forwhile 循环
for ( ; ; ) {
} // 没有分号

while (true) {
} // 没有分号

但是do...while要有分号

    1. 分支语句:ifswitchtry
if (true) {
} // 没有分号

switch () {
} // 没有分号

try {
} catch {
} // 没有分号
  • 函数的声明语句
function f() {
} // 没有分号

函数表达式仍要使用分号

var f = function f() {
};

除了这三种情况,所有语句都应该使用分号。

在没有分号时JavaScript会自动添加,这种语法特性叫"分号的自动添加"(Automatic Semicolon Insertion,简称ASI)

但是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript就不会自动添加分号。

而是否自动添加分号无法预测,很有可能导致额外的错误。

一行的起首"自增"(++)或"自减"(--),则前面会自动添加分号

不应该省略结尾的分号,还有一个原因。有些JavaScript代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。

另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。可以避免与其他脚本合并时,前面的脚本最后一行语句没有分号,导致运行出错的问题。

;var a = 1;
// ...
  1. 避免全局变量的使用,如果必须使用,考虑大写字母表示
  2. 变量声明,由于存在变量提升,许多语句会导致产生全局变量(比如for循环中)。

所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部。

  1. 建议只使用严格相等运算符(===)
  2. switch...case结构可以用对象结构代替

switch...case结构类似于goto语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。

console对象和控制台

  1. console对象是JavaScript的原生对象,可以输出各种信息到控制台
  2. console的常见用途:调试程序,显示网页代码运行时的错误信息;提供了一个命令行接口,用来与网页代码互动。
  3. 开发者工具的几个面板。
  • Elements:查看网页的 HTML 源码和 CSS 代码。
  • Resources:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。
  • Network:查看网页的 HTTP 通信情况。
  • Sources:查看网页加载的脚本源码,可进行断点debug。
  • Timeline:查看各种网页行为随时间变化的情况。
  • Performance:查看网页的性能情况,比如 CPU 和内存消耗。
  • Console:即控制台,用来运行js命令,和页面中js代码console方法的输出。
  1. console 对象的静态方法
  • console.log()console.info()console.debug()
  • console.warn(),console.error()
  • console.table()
  • console.count()
  1. debugger语句主要用于除错,作用是设置断点。

标准库

下面基本都是js原生对象的介绍,里面许多属性和方法仅了解一下即可,有需要时再查询使用

Object对象

  1. JavaScript原生提供Object对象
  2. JavaScript的所有其他对象都继承自Object对象,都是Object的实例。
  3. Object对象的原生方法分成两类:Object本身的方法("静态方法")与Object的实例方法。
  • Object对象本身的方法:直接定义在Object对象上的方法
  • Object的实例方法:定义在Object原型对象Object.prototype上的方法。它可以被Object实例直接使用。
// 本身的方法
Object.selfPrint = function (o) { console.log(o) };

// 实例方法
Object.prototype.print = function () {
  console.log(this);
};

var obj = new Object();
obj.print() // Object
  1. Object本身是一个函数,可以当作工具方法使用,将任意值转为对象。保证某个值一定是对象。
  2. Object方法无参数或为undefinednull,返回一个空对象
var obj = Object();
// 等同于
var obj = Object(undefined);
var obj = Object(null);

obj instanceof Object // true

参数是原始类型,将原始类型的值转换为对应的包装对象的实例

参数是一个对象,则返回该对象(不进行转换)

var arr = [];
var obj = Object(arr); // 返回原数组
obj === arr // true

var value = {};
var obj = Object(value) // 返回原对象
obj === value // true

var fn = function () {};
var obj = Object(fn); // 返回原函数
obj === fn // true
  • 判断变量是否为对象
function isObject(value) {
  return value === Object(value);
}

isObject([]) // true
isObject(true) // false
  1. instanceof运算符验证一个对象是否为指定的构造函数的实例
  2. Object构造函数用来生成新对象
var obj = new Object();

// 等价于
var obj = {};
  1. Object构造函数与工具方法类似。如果参数是一个对象,则直接返回该对象;如果是一个原始类型的值,则返回该值对应的包装对象
  2. Object 的静态方法
  • Object.keys()Object.getOwnPropertyNames()遍历对象的属性。两者都返回对象自身的(而不是继承的)所有属性名组成的数组。Object.keys方法只返回可枚举的属性;Object.getOwnPropertyNames还返回不可枚举的属性名。

通常使用Object.keys遍历对象属性

计算对象属性的个数

var obj = {
  p1: 123,
  p2: 456
};

Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2
  1. Object实例对象的方法:
  • Object.prototype.valueOf():返回当前对象对应的值,默认情况下返回对象本身。
  • Object.prototype.toString():返回当前对象对应的字符串形式,默认返回类型字符串。
  • Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式。
  • Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。
  • Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。
  • Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举。
  1. 数组、字符串、函数、Date对象都自定义了toString方法,覆盖了Object.prototype.toString方法。
[1, 2, 3].toString() // "1,2,3"

\'123\'.toString() // "123"

(function () {
  return 123;
}).toString()
// "function () {
//   return 123;
// }"

(new Date()).toString()
// "Fri Jul 31 2020 21:24:16 GMT+0800 (中国标准时间)"
  1. 判断数据类型

关于如何正确的判断数据类型,由于typeof仅能准确返回数值、字符串、布尔值、undefined的类型,其他返回object。所以无法借助它准确判断类型;而instanceof对于继承的对象,除了判断当前对象实例时返回true,判断继承的上级对象实例时也会返回true,并且只能判断是否是某个对象的实例,无法判断基本类型。

因此最准确的办法是利用 Object.prototype.toString方法返回对象的类型字符串 这一特点,判断一个值的类型

如下,空对象的toString方法,返回字符串object Object,第二个Object表示当前值的构造函数。

var obj = {};
obj.toString() // "[object Object]"
Object.prototype.toString.call(value)  // 对value这个值调用Object.prototype.toString方法

Object.prototype.toString可以确认一个值是什么类型。如下,实现比typeof运算符更准确的类型判断函数

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

实现判断某种类型的方法:

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

[\'Null\',
 \'Undefined\',
 \'Object\',
 \'Array\',
 \'String\',
 \'Number\',
 \'Boolean\',
 \'Function\',
 \'RegExp\'
].forEach(function (t) {
  type[\'is\' + t] = function (o) {
    return type(o) === t.toLowerCase();
  };
});

type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
  1. toLocaleString()用来实现自定义的本地字符串。如Array.prototype.toLocaleString()

    Number.prototype.toLocaleString()Date.prototype.toLocaleString()等对象自定义这个方法

属性描述对象

  1. JS提供了叫做"属性描述对象"(attributes object)的内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等。
  2. 每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
  3. 如下为属性描述对象的例子:
{
  value: 123,         // 属性的属性值  默认undefined
  writable: false,    // 属性值(value)是否可改变(可写)  默认true
  enumerable: true,   // 属性是否可遍历,默认true
  configurable: false,// 属性的可配置性,默认true 控制属性描述对象的可写性
  get: undefined,     // get该属性的取值函数(getter),默认undefined
  set: undefined      // set该属性的存值函数(setter),默认undefined。
}

定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错。

  1. Object.getOwnPropertyDescriptor()获取属性描述对象
var obj = { p: \'a\' };

Object.getOwnPropertyDescriptor(obj, \'p\')
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }
  1. Object.defineProperty()通过属性描述对象,定义或修改属性,并返回修改后的对象。
Object.defineProperty(object, propertyName, attributesObject)

参数:

  • object:属性所在的对象
  • propertyName:字符串,属性名
  • attributesObject:属性描述对象

Object.defineProperties()可以一次定义多个属性

  1. JSON.stringify方法会排除enumerable为false的属性,有时可以利用这一点。如果对象的 JSON格式输出要排除某些属性,可以把这些属性的enumerable设为false。
  2. 存取器(accessor,set-setter,get-getter)是另外定义属性的方式,定义存取器后,将会执行对应的函数。

除了defineProperty方法中通过属性描述对象定义存取器,还提供如下的写法(且这种写法configurableenumerable都为true,是可遍历的属性。更常用)

var obj = {
  get p() {
    return \'getter\';
  },
  set p(value) {
    console.log(\'setter: \' + value);
  }
};

存取器常用于:属性的值依赖对象内部数据的场合。

var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error(\'新的值必须大于等于当前值\');
  }
};

obj.next // 5

obj.next = 10;
obj.next // 10

obj.next = 5;
// Uncaught Error: 新的值必须大于当前值
  1. 对象的拷贝:

由于对象是引用类型,数据存放在堆中,栈中值存放对象的地址。默认值类型的赋值是复制给另一个变量;但引用类型的赋值是直接将引用地址复制给另一个变量,赋值引用就是常说的浅拷贝(浅拷贝的对象共用一个内存地址)。而深拷贝指的是将引用类型的数据也完全复制一份给新的变量。

对象深拷贝的基本原理就是:通过遍历对象的属性,然后将属性和递归至不是对象的属性值重新赋值为另一个对象,如果属性值是对象,则递归执行当前函数。

  • 方法一。 如下,缺点不能深拷贝function,对象存取器属性拷贝出来的是一个值
var DeepCopy = function dc(obj) {
    if (obj===null) {
      return obj;
    }
    else if (typeof obj === \'object\') {
        if (obj instanceof Array) {
            var newArr = [], i, len = obj.length;
            for (i = 0; i < len; i++) {
                newArr[i] = dc(obj[i]);
            }
            return newArr;
        } else {
            var newObj = {};
            for (var name in obj) {
               newObj[name] = dc(obj[name]);              
            }
            return newObj;
        }
    }
    // \'number\' \'string\' \'boolean\' undefined null
    return obj;
}

var objFunction=function(){
  //
}
var obj0={
  p1:1,
  get p2(){
    return this.p1;
  },
  p3:objFunction
}

var obj1=DeepCopy(obj0);
// {p1: 1, p2: 1, p3: ƒ}
//  p1: 1
//  p2: 1
//  p3: ƒ ()
obj1.p3===obj0.p3     // true
  • 方法二。使用defineProperty设定属性描述器,完成拷贝属性,可实现拷贝对象存取器属性。但是此时复制的存取器属性函数属于浅拷贝
var DeepCopy = function dc(obj) {
    if (obj===null) {
      return obj;
    }
    else if(typeof obj === \'object\'){
        if (obj instanceof Array) {
            var newArr = [], i, len = obj.length;
            for (i = 0; i < len; i++) {
                newArr[i] = dc(obj[i]);
            }
            return newArr;
        } else {
            var newObj = {};
            for (var name in obj) {
                if (obj.hasOwnProperty(name)) {
                   Object.defineProperty(
                    newObj,
                    name,
                    Object.getOwnPropertyDescriptor(obj, name)
                  );
                }
            }
            return newObj;
        }
    }
    return obj;
}
  • 方法三。如下,利用new Function构造函数实现函数function的深拷贝。这也是处理js深拷贝最全的方法了,
var DeepCopy = function dc(obj) {
    if (obj===null) {
      return obj;
    }
    else if (typeof obj === \'object\') {
        if (obj instanceof Array) {
            var newArr = [], i, len = obj.length;
            for (i = 0; i < len; i++) {
                newArr[i] = dc(obj[i]);
            }
            return newArr;
        } else {
            var newObj = {};
            for (var name in obj) {
                if (obj.hasOwnProperty(name)) {  
                  //newObj[name] = dc(obj[name]);                
                  if(typeof obj[name] === \'function\'){
                     newObj[name] = dc(obj[name]);
                  }
                  else{
                      Object.defineProperty(
                        newObj,
                        name,
                        Object.getOwnPropertyDescriptor(obj, name)
                      );                    
                  }
                }                
            }
            return newObj;
        }
    }
    else if(typeof obj === \'function\'){
      // var funStr="var f="+obj.toString()+";"
      // return new Function(funStr+"return f;");
      return new Function("return "+obj+";");
    }
    return obj;
}

obj1=DeepCopy(obj0);
// {p1: 1, p3: ƒ}p1: 1p2: (...)p3: ƒ anonymous( )get p2: ƒ p2()__proto__: Object
obj1.p3===obj0.p3  // false
obj1.p2===obj0.p2  // true
  • 方法四。对于存取器属性函数的深拷贝,可以通过getOwnPropertyDescriptor获取的属性描述器对象,判断其get和set属性,完成其函数的深拷贝

  • 方法五。还有一个简便的方法,使用JSON.stringfy()JSON.parse()序列化为json字符串然后解析为js对象,实现一个对象的深拷贝。但是它存在一个致命的问题,就是自定义的函数无法拷贝(JSON.stringfy()方法无法将函数值转为json字符串。json无法表示函数类型)

var objFunction=function(){
  //
}
var obj0={
  p1:1,
  get p2(){
    return this.p1;
  },
  p3:objFunction,
  p4:{
    p5:5
  }
}
 
var newObj = JSON.parse(JSON.stringify(obj0));
newObj   
// {p1: 1, p2: 1, p4: {…}}
//   p1: 1
//   p2: 1
//   p4:
//     p5: 5

以上对象拷贝的都是可遍历属性,且可能改变不可写的属性为可写。最最重要的是,新对象和旧对象的原型对象obj.prototype各自独立

ES6中实现对象复制的方式:比如Object.assign(浅拷贝)、展开操作符(浅拷贝)

另:Array的sliceconcat等方法不改变原数组,但是返回的也是浅拷贝了的新数组

另:$.extend方法的第一个参数给bool值表示是否深拷贝:jQuery.extend( [deep ], target, object1 [, objectN ] )

  1. 控制对象状态
  • Object.preventExtensions方法:使一个对象无法再添加新的属性
  • Object.isExtensible方法检查一个对象是否使用了Object.preventExtensions方法。检查是否可以为一个对象添加属性。
  • Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。Object.isSealed()
  • Object.freeze方法使一个对象变成常量。无法添加新属性、无法删除旧属性、也无法改变属性的值。Object.isFrozen()

上面三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。解决方案是原型也冻结住。另外一个局限是,如果属性值是对象,这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。

Array 对象

  1. Array是JavaScript的原生对象,也是一个构造函数,用来生成新数组。
var arr = new Array(2);  // 等同于 var arr = Array(2);
arr.length // 2
arr // [ empty x 2 ]
  1. Array()构造函数有很大的缺陷,不同的参数生成的结果会不一样。因此建议使用数组字面量的方式
// 不建议的方式
var arr = new Array(1, 2);

// 推荐
var arr = [1, 2];
  1. Array.isArray()静态方法,判断是否是数组
var arr = [1, 2, 3];

typeof arr // "object"
Array.isArray(arr) // true
  1. 数组对象的实例方法:
  • valueOf()返回数组本身
  • toString()返回数组的字符串形式
  • push()在数组的末端添加一个或多个元素,返回添加后的数组长度——(在数组末尾压入元素)。该方法改变原数组。
  • pop()删除数组的最后一个元素,并返回该元素——(弹出最后一个元素)。该方法改变原数组。

pushpop结合使用,可构成"后进先出"的栈结构(stack)。

var arr = [];
arr.push(1, 2);
arr.push(3);
arr.pop();
arr // [1, 2]
  • shift()删除数组的第一个元素,并返回该元素——(弹出第一个元素)。该方法改变原数组。

shift()方法可以遍历并清空一个数组。

var list = [1, 2, 3, 4];

while (list.length) {
  console.log(list.shift());
}

list // []

push()shift()结合使用,就构成了"先进先出"的队列结构(queue)。

  • unshift()在数组的第一个位置添加元素,并返回添加后的数组长度——(数组头部压入一个元素)。该方法会改变原数组。
var arr = [ \'c\', \'d\' ];
arr.unshift(\'a\', \'b\') // 4
arr // [ \'a\', \'b\', \'c\', \'d\' ]
  • join()以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。默认用逗号分隔。
var a = [1, 2, 3, 4,undefined, null];

a.join(\' \') // \'1 2 3 4  \'
a.join(\' | \') // "1 | 2 | 3 | 4 |  | "
a.join() // "1,2,3,4,,"

undefined或null或空位被转为空字符串

通过call方法,join也可以用于字符串或类似数组的对象

  • concat()用于多个数组的合并。将新数组的成员,添加到原数组成员的后部,并返回一个新数组,原数组不变。
[\'hello\'].concat([\'world\'])
// ["hello", "world"]

[\'hello\'].concat([\'world\'], [\'!\'])
// ["hello", "world", "!"]

[].concat({a: 1}, {b: 2})
// [{ a: 1 }, { b: 2 }]

concat连接的数组中有对象时,返回的浅拷贝

  • reverse()翻转数组,用于颠倒排列数组元素,返回改变后的数组。该方法将改变原数组。
  • slice()用于提取数组的一部分,返回一个新数组。原数组不变。

左闭右开,返回结果不包含end位置的元素。

arr.slice(start, end); 

省略第二个参数,会一直返回数组最后的成员;或省略全部参数,返回元素组;第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组。

slice()一个重要应用,是将类似数组的对象转为真正的数组:Array.prototype.slice.call(likeArrayObj)

  • splice()用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。该方法会改变原数组。

参数为起始位置、删除的元素个数,添加到删除位置的新元素

arr.splice(start, count, addElement1, addElement2, ...);

第二个参数设为0,可实现插入元素

var a = [1, 1, 1];

a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]

只提供第一个参数,将"剪切"到数组末尾

  • sort()对数组成员进行排序,默认按照字典顺序排序。原数组将被改变。
[\'d\', \'c\', \'b\', \'a\'].sort()
// [\'a\', \'b\', \'c\', \'d\']

[4, 3, 2, 1].sort()
// [1, 2, 3, 4]

[11, 101].sort()
// [101, 11]

[10111, 1101, 111].sort()
// [10111, 1101, 111]

通过传入一个函数,可以让sort方法按照自定义方式排序

[10111, 1101, 111].sort(function (a, b) {
  return a - b;
})
// [111, 1101, 10111]

[
  { name: "张三", age: 30 },
  { name: "李四", age: 24 },
  { name: "王五", age: 28  }
].sort(function (o1, o2) {
  return o1.age - o2.age;
})
// [
//   { name: "李四", age: 24 },
//   { name: "王五", age: 28  },
//   { name: "张三", age: 30 }
// ]

sort参数函数接受两个参数,表示进行比较的两个数组成员。如果函数的返回值大于0,表示第一个成员排在第二个成员后面;如果函数的返回值小于等于0,则第一个元素排在第二个元素前面。

自定义的排序函数应该返回数值

  • map()将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。元素组不变
var numbers = [1, 2, 3];

numbers.map(function (n) {
  return n + 1;
});
// [2, 3, 4]

numbers   // [1, 2, 3]

map参数函数的三个参数:当前成员、当前位置和数组本身。

[1, 2, 3].map(function(elem, index, arr) {
  return elem * index;
});
// [0, 2, 6]

map的第二个参数,用来绑定回调函数内部的this变量

  • forEachmap相似,对数组的所有成员依次执行参数函数,但不返回值。

如果数组遍历是为了得到返回值,可以使用map方法,否则使用forEach方法。

forEach方法无法中断执行。如果想要中断,可使用for循环、或someevery方法。

  • some()every()方法类似"断言"(assert),返回布尔值,表示数组成员是否符合某种条件

some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false。

every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回false。

借助这一点,可以循环执行数组每个元素时,some方法的参数函数中判断某个条件然后返回true,every方法的参数函数中判断某个条件然后返回false,即可起到类似for循环中break中断的作用;

var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
  console.log(elem); //执行操作
  return elem >= 3;
});
// 1
// 2
// 3
// true


arr.every(function (elem, index, arr) {
  if(elem<=3){
    console.log(elem);
    return true;
  }
  else{
    return false;
  }
});
// 1
// 2
// 3
// false

对于空数组,some方法返回false,every方法返回true,回调函数都不会执行

  • filter()过滤数组成员,满足条件的成员组成一个新数组返回——即filter的参数函数返回true的成员保留下来组成新数组。不会改变原数组。

参数函数的三个参数:当前成员,当前位置和整个数组。

[1, 2, 3, 4, 5].filter(function (elem) {
  return (elem > 3);
})

[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
  return index % 2 === 0;
});
  • reduce()reduceRight()依次处理数组的每个成员,最终累计为一个值。处理的是上一次累计值和当前元素执行结果的累计值。区别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员)。
[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log("上一次的累计值:"+a, "当前值:"+b);
  return a + b;
})
// 上一次的累计值:1 当前值:2
// 上一次的累计值:3 当前值:3
// 上一次的累计值:6 当前值:4
// 上一次的累计值:10 当前值:5
// 15

第一次执行时,累计值a是数组的第一个元素,之后就是累计值和元素值

其参数函数可接受四个变量:累积变量,默认为数组的第一个成员;当前变量,默认为数组的第二个成员;当前位置(从0开始);原数组。前两个必须

reducereduceRight的第二个参数可指定执行时的初始值

[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log("上一次的累计值:"+a, "当前值:"+b);
  return a + b;
},10)
// 上一次的累计值:10 当前值:1
// 上一次的累计值:11 当前值:2
// 上一次的累计值:13 当前值:3
// 上一次的累计值:16 当前值:4
// 上一次的累计值:20 当前值:5
// 25

空数组执行reducereduceRight时会报错,可指定第二个参数初始值解决

借助reduce(或reduceRight)可以实现一些遍历操作,比如找出字符长度最大的数组元素

function findLongest(entries) {
  return entries.reduce(function (longest, entry) {
    return entry.length > longest.length ? entry : longest;
  }, \'\');
}

findLongest([\'aaa\', \'bb\', \'c\']) // "aaa"
  • indexOf()返回给定元素在数组中第一次出现的位置,没有则返回-1。第二个参数表示搜索开始的位置
  • lastIndexOf()返回给定元素在数组中最后一次出现的位置,没有则返回-1
  1. 链式调用,如果数组方法返回的还是数组,就可以接着调用数组方法,实现链式调用
var users = [
  {name: \'tom\', email: \'tom@example.com\'},
  {name: \'peter\', email: \'peter@example.com\'}
];

users.map(function (user) {
        return user.email;
      })
      .filter(function (email) {
        return /^t/.test(email);
      })
      .forEach(function (email) {
        console.log(email);
      });
// "tom@example.com"

包装对象

  1. js的三种原始类型的值——数值、字符串、布尔值——在一定条件下会自动转为对象,这就是原始类型的"包装对象"(wrapper)
  2. "包装对象"指的是与数值、字符串、布尔值分别相对应的NumberStringBoolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
var v1 = new Number(123);
var v2 = new String(\'abc\');
var v3 = new Boolean(true);

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

v1 === 123 // false
v2 === \'abc\' // false
v3 === true // false
  1. 包装对象的设计目的:首先,使得"对象"这种类型可以覆盖JavaScript所有的值,整门语言有一个通用的数据模型。其次,使得原始类型的值也有办法调用自己的方法
  2. NumberStringBoolean作为普通函数调用时用以类型转换,将任意类型的值转为数值、字符串和布尔值等原始类型的值;作为构造函数使用(带有new)时,将原始类型的值转为对象
  3. 包装对象继承了Object对象的valueOf()——返回包装对象实例对应的原始类型的值、toString()——返回对应的字符串形式方法
  4. 原始类型与实例对象的自动转换:有时,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例

比如字符串调用length属性:

\'abc\'.length // 3

abc是一个字符串,本身不是对象,不能调用length属性。JavaScript引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换

自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。同时调用结束后,包装实例会自动销毁,所以每次调用其实都是一个新的包装对象。

var s = \'Hello World\';
s.x = 123;
s.x // undefined

如果要为字符串添加属性,只有在它的原型对象String.prototype上定义

  1. 可以在包装对象的原型对象prototype上添加自定义方法或属性

Boolean对象

  1. 通过valueOf()获取包装对象对应的原始类型值
new Boolean(false).valueOf()

Number对象

  1. Number对象的静态属性:
  • Number.POSITIVE_INFINITY:正的无限,指向Infinity
  • Number.NEGATIVE_INFINITY:负的无限,指向-Infinity
  • Number.NaN:表示非数值,指向NaN
  • Number.MIN_VALUE:表示最小正数(即最接近0的正数,在64位浮点数体系中为5e-324),相应的,最接近0的负数为-Number.MIN_VALUE
  • Number.MAX_VALUE:表示最大正数
  • Number.MAX_SAFE_INTEGER:表示能够精确表示的最大整数,即9007199254740991
  • Number.MIN_SAFE_INTEGER:表示能够精确表示的最小整数,即-9007199254740991
  1. 实例方法
  • Number.prototype.toString(),用于将一个数值转为字符串形式。该方法可以接受一个参数,表示输出的进制
(10).toString() // "10"
(10).toString(2) // "1010"
(10).toString(8) // "12"
(10).toString(16) // "a"

调用时,数值必须用括号括起来,否则js引擎会把.解读为小数点,从而混淆。任何不至于误读的写法都可以

10.toString(2)
// SyntaxError: Unexpected token ILLEGAL

10.5.toString() // "10.5"
10.5.toString(2) // "1010.1"
10.5.toString(8) // "12.4"
10.5.toString(16) // "a.8"

可使用方括号调用

10[\'toString\'](2) // "1010"

如果想将其他进制的数转为十进制,使用parseInt

  • Number.prototype.toFixed()将一个数转为指定位数的小数,然后返回个这小数对应的字符串。
(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"

由于浮点数的原因,js中小数5的四舍五入是不确定的,使用的时候必须小心。

  • Number.prototype.toExponential()将一个数转为科学计数法形式

  • Number.prototype.toLocaleString()接受地区码作为参数,返回当前数字在该地区的当地书写形式。

(123).toLocaleString(\'zh-Hans-CN-u-nu-hanidec\')
// "一二三"

toLocaleString()第二个参数是配置对象,可以定制返回的字符串。比如style属性指定输出样式,默认值decimal(十进制形式),还可取值percent(百分比)、currency(货币格式)

(123).toLocaleString(\'zh-Hans-CN\', { style: \'percent\' })
// "12,300%"
(123).toLocaleString(\'zh-Hans-CN\', { style: \'currency\', currency: \'CNY\' })
// "¥123.00"

(123).toLocaleString(\'de-DE\', { style: \'currency\', currency: \'EUR\' })
// "123,00 €"

(123).toLocaleString(\'en-US\', { style: \'currency\', currency: \'USD\' })
// "$123.00"
  • Number.prototype对象上可以自定义方法
Number.prototype.add = function (x) {
  return this + x;
};
Number.prototype.subtract = function (x) {
  return this - x;
};
(8).add(2).subtract(4)  // 6

String对象

  1. 静态方法String.fromCharCode()返回Unicode码点组成的字符串

Unicode码点不能大于0xFFFF,码点大于0xFFFF的字符占用四个字节,而JavaScript默认支持的是两个字节的字符。比如0x20BB7需要拆成两个字符来写

String.fromCharCode(0xD842, 0xDFB7)   // "