本系列基于阮一峰老师的《JavaScrip语言入门教程》或《JavaScript教程》记录整理,教程采用知识共享 署名-相同方式共享 3.0协议。这几乎是学习js最好的教程之一(去掉之一都不过分)
最好的教程而阮一峰老师又采用开源方式共享出来,之所以重新记录一遍,一是强迫自己重新认真读一遍学一遍;二是对其中知识点有个自己的记录,加深自己的理解;三是感谢这么好的教程,希望更多人阅读了解
运算符
算数运算符
- js提供了10种运算符
- 加法运算符:
x + y
- 减法运算符:
x - y
- 乘法运算符:
x * y
- 除法运算符:
x / y
- 指数运算符:
x ** y
- 余数运算符:
x % y
- 自增运算符:
++x
或者x++
- 自减运算符:
--x
或者x--
- 数值运算符:
+x
- 负数值运算符:
-x
- js中非数值可以相加,比如布尔值与数值相加,字符串相加用于连接两个字符串
true + true // 2
1 + true // 2
1 + 'a' // "1a"
false + 'a' // "falsea"
加法运算符是在运行时决定,到底是执行相加,还是执行连接。运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
加法运算符存在重载。减法、除法和乘法等运算符不会重载:所有运算子一律转为数值,再进行相应的数学运算。
- 对象的相加:运算子是对象时,会先转成原始类型的值,然后再相加。
对象默认转成原始类型的值是[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 % 2 // -1
1 % -2 // 1
可以使用绝对值,获得负数的正确余数值
// 正确的写法
function isOdd(n) {
return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
- 自增和自减运算符是一元运算符,只有一个运算子。
运算之后,变量的值发生变化,这种效应叫做运算的副作用(
side effect
)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。
自增/自减放在变量后面,会先返回变量操作前的值,再进行自增/自减操作
自增/自减放在变量之前,会先进行自增/自减操作,再返回变量操作后的值
- 数值运算符(
+
)的作用可以将任何值转为数值(与Number函数作用相同)
负数值运算符(-
),将一个值转为数值的负值
不会改变原始变量的值,而是返回新值
- 指数运算符(
**
)完成指数运算
指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2 // 512
- 赋值运算符(
Assignment Operators
)用于给变量赋值。还有复合的赋值运算符,如x += y
、x -= y
比较运算符
- 比较运算符比较两个值的大小,并返回一个布尔值。js提供了8个比较运算符
-
>
大于运算符 -
<
小于运算符 -
<=
小于或等于运算符 -
>=
大于或等于运算符 -
==
相等运算符 -
===
严格相等运算符 -
!=
不相等运算符 -
!==
严格不相等运算符
- 相等比较和非相等比较。
对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。
- 相等运算符(
==
)比较两个值是否相等,严格相等运算符(===
)比较两个值是否为“同一个值”。
如果两个值不是同一类型,严格相等运算符
===
直接返回false,而相等运算符==
会将它们转换成同一个类型,再进行比较
- 严格相等运算符:类型不同返回false;同一类型的原始类型值,会比较两者的值是否相等;复合类型的值(对象、数组、函数)比较的是是否指向同一个地址;undefined和null与自身严格相等
两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值
var obj1 = {};
var obj2 = {};
obj1 > obj2 // 比较的是值 false
obj1 < obj2 // 比较的是值 false
obj1 === obj2 // 比较的是地址 false
相等运算符比较是隐含了类型转换,建议最好只使用严格相等运算符(===
)。
布尔运算符
- 布尔运算符用于将表达式转为布尔值。一共有4个
- 取反运算符:
!
- 且运算符:
&&
- 或运算符:
||
- 三元运算符:
?:
- 取反运算符将布尔值变为相反值。两次取反就是将一个值转为布尔值的简便写法
- 且运算符
&&
常用于多个表达式的求值
且运算符
&&
运算规则是:如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值。
&&
且运算可以用来取代if
语句
if (i) {
doSomething();
}
// 等价于
i && doSomething();
- 或运算符(
||
)也用于多个表达式的求值。
或运算符
||
的运算规则是:如果第一个运算子的布尔值为true
,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false
,则返回第二个运算子的值。
或运算符常用于为一个变量设置默认值。
function saveText(text) {
text = text || '';
// ...
}
// 或者写成
saveText(this.text || '')
- 且运算符和或运算符,这种通过第一个表达式(运算子)的值,控制是否运行第二个表达式(运算子)的机制,就称为“短路”(
short-cut
) - 三元条件运算符(
?:
)是js中唯一一个需要三个运算子的运算符
二进制位运算符
- 二进制位运算符用于直接对二进制位进行计算,一共有7个:
-
二进制或运算符(or):符号为
|
,表示若两个二进制位都为0,则结果为0,否则为1。 -
二进制与运算符(and):符号为
&
,表示若两个二进制位都为1,则结果为1,否则为0。 -
二进制否运算符(not):符号为
~
,表示对一个二进制位取反。 -
异或运算符(xor):符号为
^
,表示若两个二进制位不相同,则结果为1,否则为0。 -
左移运算符(left shift):符号为
<<
, -
右移运算符(right shift):符号为
>>
, -
头部补零的右移运算符(zero filled right shift):符号为
>>>
,
- 位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。虽然在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数
利用这个特性,可以写出一个函数,将任意数值转为32位整数。
function toInt32(x) {
return x | 0;
}
- 位运算符可以用作设置对象属性的开关。(开关作用有些抽象,但很精巧)
假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。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
// ...
}
- 假设需要打开
A
、B
、D
三个开关,可以先构造一个掩码变量,然后通过二进制或运算掩码变量,可以确保打开这三个开关
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和逗号运算符
- void运算符,执行一个表达式,然后不返回任何值,或者返回
undefined
void 0 // undefined
void(0) // undefined 推荐写法
void运算符的优先级很高,使用括号避免错误
var x = 3;
void (x = 5) //undefined
x // 5
- 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>
- 逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10
用途是:在返回一个值之前,进行一些辅助操作。
var value = (console.log('Hi!'), true);
// Hi!
value // true
运算顺序
- 运算符优先级别(
Operator Precedence
)高的先执行 - 圆括号
()
用来提高运算的优先级(它的优先级最高),即圆括号中的表达式会第一个运算
圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。
函数放在圆括号中,会返回函数本身。圆括号紧跟在函数的后面,表示调用函数。
圆括号之中,只能放置表达式
- "左结合"(
left-to-right associativity
)运算符会先从左向右运算
"右结合"(right-to-left associativity
)运算符会先从右向左运算
js中赋值运算符(=
)、三元条件运算符(?:
)、指数运算符(**
)是"右结合"的
语法
数据类型的转换
- JavaScript 是一种动态类型语言,变量的类型无法在编译阶段确定,必须在运行时才能知道。而同时js的变量类型又可以随意改变,因此又属于弱类型语言
- JS中的运算符对数据类型有要求。因此常常发生类型自动转换
- 强制类型转换主要指使用
Number()
、String()
和Boolean()
手动将任意类型的值,分别转换成数字、字符串或者布尔值。 -
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方法返回的是对象,就报错。
自定义valueOf
或toString
Number({
valueOf: function () {
return 2;
}
})
// 2
Number({
toString: function () {
return 3;
}
})
// 3
Number({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// 2
-
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"
-
Boolean()
转换为布尔值,规则简单,除了下面6个值结果为false,其余全部为true
undefined
、null
、0
(包含-0
和+0
)、NaN
、''
(空字符串)和false
所有对象(包括空对象)的转换结果都是true
,包括false
对应的布尔对象new Boolean(false)
也是true
- js中数据类型自动转换发生的情况:一、不同类型的数据相互运算时会自动转换。二、对非布尔值类型的数据求布尔值时。三、对非数值类型的值使用一元运算符(即
+
和-
)。转换时的规则是:预期什么类型的值,就调用该类型的转换函数。如果该位置既可以是字符串,又可以是数值,则默认转为数值 - JavaScript在预期为布尔值的地方(比如if语句的条件部分),会将非布尔值的参数自动转换为布尔值。系统内部会自动调用
Boolean
函数。
如下两个方法将一个表达式转为布尔值
// 写法一
expression ? true : false
// 写法二
!! expression
- 除了加法运算符(
+
)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。
null
数值0
,undefined
数值NaN
错误处理机制
- JavaScript原生提供
Error
构造函数,所有抛出的错误都是这个构造函数的实例。当发生错误时,js引擎抛出Error
实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。
var err = new Error('出错了');
err.message // "出错了"
-
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
-
Error
实例是最一般的错误类型,js还提供Error
的6个派生对象
-
SyntaxError
对象:解析代码时发生的语法错误 -
ReferenceError
对象:引用一个不存在的变量时发生的错误。 -
RangeError
对象:一个值超出有效范围时发生的错误。 -
TypeError
对象:变量或参数不是预期类型时发生的错误。 -
URIError
对象:URI
相关函数的参数不正确时抛出的错误。主要encodeURI()
、decodeURI()
、encodeURIComponent()
、decodeURIComponent()
、escape()
和unescape()
。 -
EvalError
对象:已不再使用
- 自定义错误
function UserError(message) {
this.message = message || '默认信息';
this.name = 'UserError';
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
-
throw
语句:手动中断程序执行,抛出一个错误。
if (true) {
throw new Error('x 必须为正数');
}
// Uncaught Error: x 必须为正数
// at <anonymous>:2:9
throw
可以抛出任何类型的值
-
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);
}
// ...
}
-
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();
}
编程风格
- "编程风格"(
programming style
)指的是编写代码的样式规则。
你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格 - 编程风格主要考虑的几点:缩进(
indent
)、区块(block
)、圆括号(parentheses
)、行尾的分号、变量声明、严格相等、语句的合并书写等 - 使用
{}
代码块时,js中要使用左大括号{
紧挨着语句在同一行中,不要换行写。这是因为JavaScript会自动添加句末的分号,从而产生一些难以察觉的错误。
block {
// ...
}
如下return
语句其实会变成两句,从而导致出问题
return
{
key: value
};
// 相当于
return;
{
key: value
};
// 正确写法
return {
key : value
};
- 行尾的分号:分号表示一条语句的结束。js允许省略。
有三种情况,语法规定不需要在结尾添加分号。如果添加,js引擎将分号解释为空语句
-
for
和while
循环
-
for ( ; ; ) {
} // 没有分号
while (true) {
} // 没有分号
但是do...while
要有分号
- 分支语句:
if
,switch
,try
- 分支语句:
if (true) {
} // 没有分号
switch () {
} // 没有分号
try {
} catch {
} // 没有分号
- 函数的声明语句
function f() {
} // 没有分号
函数表达式仍要使用分号
var f = function f() {
};
除了这三种情况,所有语句都应该使用分号。
在没有分号时JavaScript会自动添加,这种语法特性叫"分号的自动添加"(Automatic Semicolon Insertion
,简称ASI
)
但是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript就不会自动添加分号。
而是否自动添加分号无法预测,很有可能导致额外的错误。
一行的起首"自增"(++)或"自减"(--),则前面会自动添加分号
不应该省略结尾的分号,还有一个原因。有些JavaScript代码压缩器(
uglifier
)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。可以避免与其他脚本合并时,前面的脚本最后一行语句没有分号,导致运行出错的问题。
;var a = 1;
// ...
- 避免全局变量的使用,如果必须使用,考虑大写字母表示
- 变量声明,由于存在变量提升,许多语句会导致产生全局变量(比如
for
循环中)。
所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部。
- 建议只使用严格相等运算符(
===
) -
switch...case
结构可以用对象结构代替
switch...case
结构类似于goto
语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。
console对象和控制台
-
console
对象是JavaScript的原生对象,可以输出各种信息到控制台 -
console
的常见用途:调试程序,显示网页代码运行时的错误信息;提供了一个命令行接口,用来与网页代码互动。 - 开发者工具的几个面板。
-
Elements
:查看网页的 HTML 源码和 CSS 代码。 -
Resources
:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。 -
Network
:查看网页的 HTTP 通信情况。 -
Sources
:查看网页加载的脚本源码,可进行断点debug。 -
Timeline
:查看各种网页行为随时间变化的情况。 -
Performance
:查看网页的性能情况,比如 CPU 和内存消耗。 -
Console
:即控制台,用来运行js命令,和页面中js代码console方法的输出。
-
console.log()
,console.info()
,console.debug()
console.warn(),console.error()
console.table()
console.count()
-
debugger
语句主要用于除错,作用是设置断点。
标准库
下面基本都是js原生对象的介绍,里面许多属性和方法仅了解一下即可,有需要时再查询使用
Object对象
- JavaScript原生提供
Object
对象 - JavaScript的所有其他对象都继承自
Object
对象,都是Object
的实例。 -
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
-
Object
本身是一个函数,可以当作工具方法使用,将任意值转为对象。保证某个值一定是对象。 -
Object
方法无参数或为undefined
、null
,返回一个空对象
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
-
instanceof
运算符验证一个对象是否为指定的构造函数的实例 -
Object
构造函数用来生成新对象
var obj = new Object();
// 等价于
var obj = {};
-
Object
构造函数与工具方法类似。如果参数是一个对象,则直接返回该对象;如果是一个原始类型的值,则返回该值对应的包装对象 - 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
-
Object
实例对象的方法:
-
Object.prototype.valueOf()
:返回当前对象对应的值,默认情况下返回对象本身。 -
Object.prototype.toString()
:返回当前对象对应的字符串形式,默认返回类型字符串。 -
Object.prototype.toLocaleString()
:返回当前对象对应的本地字符串形式。 -
Object.prototype.hasOwnProperty()
:判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。 -
Object.prototype.isPrototypeOf()
:判断当前对象是否为另一个对象的原型。 -
Object.prototype.propertyIsEnumerable()
:判断某个属性是否可枚举。
- 数组、字符串、函数、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 (中国标准时间)"
- 判断数据类型
关于如何正确的判断数据类型,由于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
-
toLocaleString()
用来实现自行的本地字符串。如Array.prototype.toLocaleString()
、Number.prototype.toLocaleString()
、Date.prototype.toLocaleString()
等对象自定义这个方法
属性描述对象
- JS提供了叫做"属性描述对象"(
attributes object
)的内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等。 - 每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
- 如下为属性描述对象的例子:
{
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属性,否则会报错。
-
Object.getOwnPropertyDescriptor()
获取属性描述对象
var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p')
// Object { value: "a",
// writable: true,
// enumerable: true,
// configurable: true
// }
-
Object.defineProperty()
通过属性描述对象,定义或修改属性,并返回修改后的对象。
Object.defineProperty(object, propertyName, attributesObject)
参数:
- object:属性所在的对象
- propertyName:字符串,属性名
- attributesObject:属性描述对象
Object.defineProperties()
可以一次定义多个属性
-
JSON.stringify
方法会排除enumerable
为false的属性,有时可以利用这一点。如果对象的JSON
格式输出要排除某些属性,可以把这些属性的enumerable
设为false。 - 存取器(
accessor
,set-setter,get-getter)是另外定义属性的方式,定以存取器后,将会执行对应的函数。
除了defineProperty
方法中通过属性描述对象定义存取器,还提供如下的写法(且这种写法configurable
和enumerable
都为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: 新的值必须大于当前值
- 对象的拷贝:
由于对象是引用类型,数据存放在堆中,栈中值存放对象的地址。默认值类型的赋值是复制给另一个变量;但引用类型的赋值是直接将引用地址复制给另一个变量,赋值引用就是常说的浅拷贝(浅拷贝的对象共用一个内存地址)。而深拷贝指的是将引用类型的数据也完全复制一份给新的变量。
对象深拷贝的基本原理就是:通过遍历对象的属性,然后将属性和递归至不是对象的属性值重新赋值为另一个对象,如果属性值是对象,则递归执行当前函数。
-
方法一。 如下,缺点不能深拷贝
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的slice
和concat
等方法不改变原数组,但是返回的也是浅拷贝了的新数组
另:$.extend
方法的第一个参数给bool值表示是否深拷贝:jQuery.extend( [deep ], target, object1 [, objectN ] )
- 控制对象状态
-
Object.preventExtensions
方法:使一个对象无法再添加新的属性 -
Object.isExtensible
方法检查一个对象是否使用了Object.preventExtensions
方法。检查是否可以为一个对象添加属性。 -
Object.seal
方法使得一个对象既无法添加新属性,也无法删除旧属性。Object.isSealed()
-
Object.freeze
方法使一个对象变成常量。无法添加新属性、无法删除旧属性、也无法改变属性的值。Object.isFrozen()
上面三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性。解决方案是原型也冻结住。另外一个局限是,如果属性值是对象,这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容。
Array 对象
-
Array
是JavaScript的原生对象,也是一个构造函数,用来生成新数组。
var arr = new Array(2); // 等同于 var arr = Array(2);
arr.length // 2
arr // [ empty x 2 ]
-
Array()
构造函数有很大的缺陷,不同的参数生成的结果会不一样。因此建议使用数组字面量的方式
// 不建议的方式
var arr = new Array(1, 2);
// 推荐
var arr = [1, 2];
-
Array.isArray()
静态方法,判断是否是数组
var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true
- 数组对象的实例方法:
-
valueOf()
返回数组本身 -
toString()
返回数组的字符串形式 -
push()
在数组的末端添加一个或多个元素,返回添加后的数组长度——(在数组末尾压入元素)。该方法改变原数组。 -
pop()
删除数组的最后一个元素,并返回该元素——(弹出最后一个元素)。该方法改变原数组。
push
和pop
结合使用,可构成"后进先出"的栈结构(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()
一个重要应用,是将类似数组的对象转为真正的数组。
-
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
变量
-
forEach
与map
相似,对数组的所有成员依次执行参数函数,但不返回值。
如果数组遍历是为了得到返回值,可以使用map方法,否则使用forEach方法。
forEach方法无法中断执行。如果想要中断,可使用for循环、或some
、every
方法。
-
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开始);原数组。前两个必须
reduce
和reduceRight
的第二个参数可指定执行时的初始值
[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
空数组执行reduce
或reduceRight
时会报错,可指定第二个参数初始值解决
借助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
。
- 链式调用,如果数组方法返回的还是数组,就可以接着调用数组方法,实现链式调用
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"
包装对象
- js的三种原始类型的值——数值、字符串、布尔值——在一定条件下会自动转为对象,这就是原始类型的"包装对象"(
wrapper
) - "包装对象"指的是与数值、字符串、布尔值分别相对应的
Number
、String
、Boolean
三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
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
- 包装对象的设计目的:首先,使得"对象"这种类型可以覆盖JavaScript所有的值,整门语言有一个通用的数据模型。其次,使得原始类型的值也有办法调用自己的方法。
-
Number
、String
和Boolean
作为普通函数调用时用以类型转换,将任意类型的值转为数值、字符创和布尔值等原始类型的值;作为构造函数使用(带有new
)时,将原始类型的值转为对象 - 包装对象继承了
Object
对象的valueOf()
——返回包装对象实例对应的原始类型的值、toString()
——返回对应的字符串形式方法 - 原始类型与实例对象的自动转换:有时,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。
比如字符串调用length
属性:
'abc'.length // 3
abc
是一个字符串,本身不是对象,不能调用length
属性。JavaScript引擎自动将其转为包装对象,在这个对象上调用length
属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。
自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。同时调用结束后,包装实例会自动销毁,所以每次调用其实都是一个新的包装对象。
var s = 'Hello World';
s.x = 123;
s.x // undefined
如果要为字符串添加属性,只有在它的原型对象String.prototype
上定义
- 可以在包装对象的原型对象
prototype
上添加自定义方法或属性
Boolean对象
- 通过
valueOf()
获取包装对象对应的原始类型值
new Boolean(false).valueOf()
Number对象
- 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
。
- 实例方法
-
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对象
- 静态方法
String.fromCharCode()
返回Unicode码点组成的字符串
Unicode码点不能大于0xFFFF
,码点大于0xFFFF
的字符占用四个字节,而JavaScript默认支持的是两个字节的字符。比如0x20BB7
需要拆成两个字符来写
String.fromCharCode(0xD842, 0xDFB7) // "