教程
https://wangdoc.com/javascript/types/general.html
1 概述
1.1 数据类型分类
- 数值,number, 整数和小数
- 字符串,string, 文本 Hello World
- 布尔值,bool
- undefined, 表示未定义或不存在,比如仅是声明的变量它的值就是 undefined
- null,表示空值,此处的值为空
- 对象,object,各种值组成的集合
number , string & bool 合成为原始类型(primitive type),因为它们是最基本的数据类型,不能再细分了。
对象是合成类型(complex type),因为一个对象往往由多个原始类型的值合成,是一个存放各种值的容器。
undefined 和 nulll,一般是将它们看为两个特殊值。
对象是最复杂的数据类型,又可以分为三个子类型:
- 狭义的对象(object)
- 数组 (array)
- 函数 (function)
狭义的对象和数组是两种不同的数据组合方式,教程中的对象都是指的是狭义的对象。
函数其实是处理数据的方法,JavaScript 把它当做一种数据类型,可以赋值给变量,这给编程带来灵活性,也为 JS 的函数式编程 奠定基础。Python 也可以把函数赋值给变量。Go 也是支持把函数赋值给变量的。
1.2 typeof 运算符
JavaScript 有三种方法,确定一个值到底是什么类型
- typeof 运算符
- instanceof 运算符
- Object.prototype.toString 方法
typeof 可以返回一个值的数据类型。(这个 typeof 居然不是驼峰命名,也是一大怪)
数值、字符串、布尔值分别返回 "number" / "string" / "boolean"
typeof 123 // "number"
typeof "abc" // "string"
typeof false // "boolean"
函数返回 "function"
function f() {}
typeof f // "function"
undefined 返回 "undefined"
typeof undefined // "undefined"
typeof 的使用场景
可以用来检测一个没有声明的变量
// 上下文中没有对 v 的任何定义
// 如果直接调用 v,会:Uncaught ReferenceError: v is not defined
typeof v // "undefined"
实际编程中,这个特点通常放在判断语句中。
// 错误的写法
if (v){
....
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined"){
....
}
对象返回 "object"
typeof window / document // "object"
typeof {} // "object"
typeof [] // "object"
数组的类型也是 “object”,这表示在 JS 内部,数组本质上是一种特殊的对象。
null 返回 "object"
typeof null
// "object"
null 的类型是 "object" 这是历史原因造成的。JS 语言第一版只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),当时只是把 null 当成 object 的一种特殊值。后来 null 独立出来,作为一种单独的数据类型,但是为了兼容以前的代码,typeof null 返回 "object" 就没有办法改了。
2 null, undefined 和 布尔值
2.1 undefined 和 null 的区别
教程 http://www.ruanyifeng.com/blog/2014/03/undefined-vs-null.html
- 相似性
将一个变量赋值为 undefined 或 null,几乎没有区别:
let a = undefined;
let b = null;
使用相等运算符,会直接报告它们相等
undefined == null // true
它们的布尔值都是 false
Boolean(null) // false
Boolean(undefined) // false
- 差别
null 表示一个空对象,在转为数值时可以转化为 0; undefined 是一个“此处无定义”的原始值,转化为 NaN
Number(null) // 0
5 + null // 5
Number(undefined) // NaN
- undefined 表示未定义,下面是 undefined 的典型场景
- 变量声明了,但是没有赋值
var i;
i // undefined
- 调用函数时,应该提供的参数没有提供,该参数等于 undefined; 这一点算得上是拙劣的设计了,如果是其他的语言会直接抛出异常。
function f(x){
return x;
}
f() // undefined
- 对象没有赋值的属性
var o = new Object();
o.p // undefined
- 函数没有返回值,默认返回 undefined
function f() {}
f() // undefined
2.1 布尔值
Python 和 JS 都会对一些值进行隐式的布尔值转换。Go 和 Java 这种强类型语言则不会
- undefined
- null
- false
- 0
- NaN
- "" 或 ‘’(空字符串)
这几种值的布尔值都是 undefined。在应用中,可以写出这种形式:
if (''){
}
注意,空 [] 和 {} 对应的布尔值是 true, 这也是 JS 的特性:
Boolean([]) // true
Boolean({}) // true
3 数值
https://wangdoc.com/javascript/types/number.html
3.1 概述
3.1.1 整数和浮点数
JavaScript 内部,所有数字都是以 64 位浮点数形式存储,即使整数也是如此。所以,在 1 与 1.0 是相同的,是同一个数。
1 === 1.0 // true
这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64 位浮点数)。容易造成混淆的是,某些运算是只有整数才能完成,此时 JavaScript 会自动把 64 位浮点数转为 32 位整数,然后再进行运算。
由于浮点数不是精确的值,所以涉及到小数的比较和运算要特别小心。
0.1 + 0.2
// 0.300000000000000004
0.3 / 0.1
// 2.999999999999999996
0.3 - 0.2
// 0.099999999999999998
3.1.2 数值精度
精度最多只能到 53 个二进制,这意味着,绝对值小于等于 2 的 53 次方的整数。
Math.pow(2, 53)
// 9007199254740992
3.1.3 数值范围
如果一个数大于等于 2 的 1024 次方,那么就会发生 “正向溢出”,即返回 JavaScript 无法表示这么大的数,这时就会返回 Infinity.
Math.pow(2, 1024) // Infinity
如果一个数小于等于 2 的 -1075 次方,那么就会发生 "负向溢出",JS 无法表示这么小的数,会直接返回0
Math.pow(2, -1075) // 0
JavaScript 提供 Number 对象的 MAX_VALUE 和 MIN_VALUE 属性,返回可以表示的具体的最大值和最小值
Number.MAX_VALUE // 1.7....
Number.MIN_VALUE // 5e-324
3.2 数值的表示法
JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如 35 (十进制)和 0xFF (十六进制)
123e3 // 123000
123e-3 // 0.123
-3.1E+12
.1e-23
科学计数法允许字母 e 和 E 的后面,跟着一个整数,表示这个数值的指数部分。
以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式表示。
1)小数点前的数字多于 21 位。
1111111111111111111111111111111111111111111111111
// 1.1111111111111112e+48
- 小数点后面的零多于5个。
// 小数点后紧跟 5 个以上的零,
// 就自动转化为科学计数法
0.0000003 // 3e-7
3.3 数值的进制
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
- 十进制:没有前导 0 的数值。
- 八进制:有前缀
0o
或00
的数值,或者有前导0、且只用到 0-7 的八个阿拉伯数值。 - 十六进制:有前缀
0x
或0X
的数值 - 二进制:有前缀
0b
或0B
的数值。
3.4 特殊数值
JavaScript 提供了几个特殊的数值。
3.4.1 正零和负零
JavaScript 的 64 位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连 0
也不例外。
JavaScript 中存在两个0:一个是+0,一个是-0,区别就是64位浮点数表示法的符号位不同。它们是等价的。
-0 === +0 //true
0 === -0 //true
0 === +0 //true
几乎在所有场合,正零和负零都会被当作正常的 0
+0 // 0
-0 // 0
(-0).toString() // '0'
(+0).toString() // '0'
唯一有区别的场合是,+0 或 -0 当作分母,返回的值是不相等的。
(1 / +0) === (1 / -0) // false
上面的代码之所以出现这样的结果,是因为除以正零得到 +Infinity
,除以负零得到-Infinity
,这两者是不相等的。
3.4.2 NaN
(1) 含义
NaN 是 JS 的特殊值,表示 “非数字”(Not a Number), 主要出现在将字符串解析成数字出错的场合。
5 - 'x' // NaN
上面的代码运行时,会自动将字符串 x
转化为数值,但是由于 x
不是数值,所以最终得到的结果是 NaN
,表示它是非数字.
NaN 不是特殊的数据类型,而是一个特殊数值,它的数据类型依然属于 Number
,使用 typeof
运算符可以看清楚。
typeof NaN // 'number'
(2) 运算规则
NaN 不等于任何值,包括它本身。
NaN === NaN // false
数组的 indexOf 方法内部使用的是严格相等运算符,所以该方法对 NaN不成立。
[NaN].indexOf(NaN) // false
NaN 在的布尔值是 false
Boolean(NaN) // false
NaN 与任何数(包括它自己)的运算,得到的结果都是 NaN.
NaN + 32 // NaN
NaN + NaN // NaN
3.4.2 Infinity
(1) 含义
Infinity
表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非 0 数值除以 0,得到 Infinity
。
// 场景一
Math.pow(2, 1024) // Infinity
场景二
1/ 0 // Infinity
Infinity 有正负之分,Infinity
表示正的无穷,-Infinity
表示负的无穷。
Infinity === -Infinity // false
1 / -0 // -Infinity
由于数值正向溢出和负向溢出和被0除,JavaScript 都不报错,而是返回 Infinity, 所以单纯的数学运算机会没有可能抛出错误。
Infinity
大于一切数值(除了 NaN
), -Infinity
小于一切数值(除了 NaN
)
Infinity > 111111111111111111111111 //true
-Infinity < -11111111111111 // true
Infinity
与 NaN
比较,总是返回 false
Infinity < NaN // false
Infinity > NaN // false
(2) 运算规则
Infinity 的运算规则,符合无穷的数学计算规则。
5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0
0 乘以 Infinity,返回 NaN; 0 除以 Infinity, 返回 0; Infinity 除以0,返回 Infinity。
0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity
Infinity 加上或乘以 Infinity, 返回的还是 Infinity。
Infinity + Infinity // Infinity
Infinity * Infinity // Infinity
Infinity 减去或除以 Infinity, 得到 NaN
Infinity - Infinity // NaN
Infinity / Infinity // NaN
Infinity 与 null 计算时, null 会转成0,等同于与 0 的计算。
null * Infinity // NaN
null / Infinity // 0
Infinity / null // Infinity
Infinity 与 undefined 计算,返回的都是 NaN
undefined + Infinity // NaN
undefined - Infinity // NaN
undefined * Infinity // NaN
undefined / Infinity // NaN
Infinity / undefined // NaN
3.5 与数值相关的全局方法
全局方法,也就是像 Python 那样的内置函数
3.5.1 parseInt()
(1) 基本用法
parseInt
方法用于将字符串转为整数
parseInt('123') // 123
如果字符串头部或尾部有空格,空格会被自动去除
parseInt(" 123 ") // 123
如果 parseInt
的参数不是字符串,则会先转为字符串再转化。
parseInt(1.23) // 1
// 等同于
parseInt('1.23') // 1
字符串转化为整数的时候,是一个个字符依次转化下去的,如果遇到不能转化为数字的字符,就不再进行下去,返回已经转好的部分。——这个特性真是中二!
parseInt("12e3") // 12
如果字符串的第一个字符就不能转化为数字的话,则会返回NaN。(第一位为数字的正负号除外)
parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') NaN
如果字符串以 0x
或 0X
开头,parseInt
会将其按照十六进制数解析。
parseInt('0x10') // 16
如果字符串以 0
开头,将其按照 10 进制解析。
parseInt('011') // 11
对于那些会自动转为科学计数法的数字, parseInt
会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。
parseInt(111111111111111111111111111111) // 1
// 等同于
parseInt("1.11111111112e+54") // 1
parseInt(0.000008) // 8
parseInt('8e-7') //8
(2) 进制转换
parseInt
方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt
的第二个参数为10,表示默认是十进制转化为十进制。
parseInt('1000') // 1000
// 等同于
parseInt('1000', 10) // 1000
下面是转化指定进制的数的例子
parseInt('1000', 2) // 2
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512
如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回 NaN 。如果第二个参数是0、undefined 和 null,则直接忽略
parseInt('10', 37) // NaN
parseInt('1000', 2.2) // 2
3.5.2 parseFloat()
parseFloat() 也是一个内置方法,作用是跟 parseInt
一样的。
3.5.3 isNaN()
isNaN 方法可以用来一个值是否为 NaN
isNaN(NaN) // true
isNaN(123) // false
但是,isNaN
只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成 NaN
,所以最后返回 true
,这一点要特别引起注意。也就是说,isNaN
为 true
的值,有可能不是 NaN
,而是一个字符串。
isNaN('Hello') // true
isNaN(Number('Hello')) // true
出于同样的原因,对于对象和数组,isNaN 也返回 true
isNaN({}) // true
// 等同于
isNaN(Number({})) // true
// ...
isNaN(['xzy']) // true
// 等同于
isNaN(Number(['xzy'])) //true
但是,对于空数组和只有一个数值成员的数组,isNaN 返回 false。
isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false
上面代码之所以返回 false
,原因是这些数组能被 Number
函数转成为数值,请参见《数据类型转换》一章。
因此,使用isNaN
之前,最好判断一下数据类型。
function myIsNaN(value){
return typeof value === 'number' && isNaN(value);
}
myIsNaN(NaN) // true
判断 NaN
更可靠的方法是,利用 NaN
为唯一不等于自身的值的这个特点,进行判断:
function myIsNaN(value){
return value !== value;
}
3.5.4 isFinite()
isFinite
方法返回一个布尔值,表示某个值是否正常的数值。
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true
除了 Infinity
/ -Infinity
/ NaN
和 undefined
这个值会返回 false
,isFinite
对于其他的数值都会返回 true
4 字符串
4.1 概述
4.1.1 定义
字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。
'abc'
"abc"
在 Java 、Go 等静态语言中,有字符这个概念,用单引号引起来,字符串是字符数组的语法糖用双引号引起来。
可以用反斜杠来转义。
"Did she say \"Hello\" ?"
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号。
let longStr = `long \ long`
可以使用 \
多行定义字符串,在 es6 中可以使用 `` 来定义多行字符串。
也可以使用 + 连接多个单行字符串。
还有一种利用多行注释的变通方法。
(function(){/*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
4.1.2 转义
反斜杠 () 在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些:
-
\0
: null -
\b
: 后退键 -
\f
: 换页符 -
\n
: 换行符 -
\r
: 回车键 -
\t
: 制表符 - \ : 反斜杠
- ' : 单引号
- " : 双引号
反斜杠还有三种特殊用法。
-
\HHH
反斜杠后面紧跟三个八进制数(000
到377
),代表一个字符。HHH
对应该字符的 Unicode 码点,比如\251
表示版权符号。显然,这种方法只能输出 256 种字符。 -
\xHH
\x
后面紧跟两个十六进制数(00
到FF
),代表一个字符。HH
对应该字符的 Unicode 码点,比如\xA9
表示版权符号。 -
\uXXXX
\u
后面紧跟四个十六进制数(0000
到FFFF
),代表一个字符。XXXX
对应该字符的 Unicode 码点,比如\u00A9
表示版权符号。
如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。
'\a' // "a"
如果字符串的正常之中,需要包含反斜杠,用来对自身转义。
"Prev \\ Next"
// "Prev \ Next"
4.1.3 字符串与数组
字符串可以视为字符数组,可以进行索引。(阮大说的真费劲)
如果方括号中不是数字,或者索引越界,则会返回 undefined, 而不是强类型语言一样报错。
let s = 'Hello';
s[0] // "h"
s["0"] // "h"
s[null] // undefined
s[1000] // undefined
字符串只是和数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
对字符串进行删改的操作会失效,但是不会出错。
let s = "hello";
delete s[0];
s[1] = 'a';
s // hello
4.1.4 length 属性
length 属性返回字符串的长度,该属性也是无法改变的
let s = "hello";
s.length // 5
s.length = 10
s.length // 5
4.2 字符集
JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成 \uxxxx
的形式,其中xxxx
代表该字符的 Unicode 码点。比如,\u0049
代表版权符号。
let s = '\u00A9';
s // "©"
解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式。输出给用户的时候,所以字符都会转成字面形式。
let f\u006F\u006F = 'abc';
foo // "abc"
上面代码中,第一行的变量名是 foo
是 Unicode 形式表示,第二行是字面形式表示。JavaScript 会自动识别。
每个字符在 JavaScript 内部以 16 位(2个字节)的 UTF-16 格式存储。也就是说,JavaScript 的单位字符长度固定为 16 位长度,2个字节。
UTF-16 有两种长度:对于码点在 U+0000
到U+FFFF
之间的字符,长度为16位(2个字节);对于码点在 U+10000
到 U+10FFFF
之间的字符,长度为 32 位(即 4 个字节)。
JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。所以对于四字节字符,浏览器会正确识别这是一个字符,但是 JS 会认为这个两个字符。
总之,JS 返回的字符串长度可能是不正确的。
4.3 Base64 转码
有时候,文本里面包含一些不可打印的符号,比如 ASCII 码 0 到 31 的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另外一个场景是,有时需要以文本格式传递二进制数据,可以使用 Base64 编码。
Base64 就是一种编码方式,可以将任意值转化成 0~9 \ A-Z \ a-z + 和 / 这64个字符组合的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript 原生提供了两个 Base64 相关的方法。
-
btoa()
: 任意值转为 Base64 编码。 -
atob()
: Base64 编码转为原来的值。
let str = "Hello";
btoa(str) // SGVsbG8=
atob("SGVsbG8=") // Hello
注意,这两个方法不适合非 ASCII 码的字符,会报错。
btoa('你好') // 报错,The string to be encoded contains characters outsie of the Latin1 range (字符串包含了非 Latin1(拉丁) 字符)
要将非 ASCII 码字符转 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
function b64Encode(str){
return btoa(encodeURIComponent(str));
}
function b64Decode(str){
return decodeURIComponent(atob(str));
}
4.4 ASCII 与 非ASCII 转化
// 转化为 ASCII
encodeURIComponent(str)
// 将 ASCII 转化为 非 ASCII
decodeURIComponent(str)
5 对象
https://wangdoc.com/javascript/types/object.html
5.1 概述
5.1.1 生成方法
对象( Object ) 是 JavaScript 语言的核心,也是最重要的数据类型。
简单来说,对象就是一组键值对的“集合”
5.1.2 键名
键名是数值时,会自动转化为字符串。但是打印出来的时候看不出来。
let d = {1:"a"}
如果键名不符合变量名的命名规则,如第1位是数值或者以空格开头,那么需要用引号引起来。
d["1px"] = "c"
对象的每一个键名都又称为“属性”(property),键值可以是任何数据类型。如果一个属性的值是函数时,这个属性一般被称作方法,它可以像函数一样被调用。
let b = {
f : (x)=>{return x}
}
b.f(3) // 3
如果属性的值是一个对象,就形成了链式引用。
let a = {b:{c:3}}
a.b.c //3
5.1.3 对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是指向同一个内存地址。如果修改其中一个变量,会影响到其他所有的变量。
let a = {w:1}
let b = a;
b.w = 2
a.w // 2
这个问题很经典,python 里面的深拷贝和浅拷贝也是这个样子,Java 和 Go 也有类似的问题。这个问题的关键是,要分清这个值是什么类型,如果值是原始类型/基础类型等,那么分别赋予不同的变量名就会发生值拷贝,修改彼此不发生影响。如果是值引用类型,这个类型通常是个容器,里面的成员是个指向真实值地址的引用。这时修改这个容器,就会影响所有的变量名。
5.1.4 表达式还是语句
{foo:123}
如果行首出现这行代码,会有两种含义:
这是一个对象,
这是一个语句,表示是一个代码块,里面一个标签 foo ,指向代码123。
所以为了避免歧义,行首是大括号时,最好在前面加上括号。
5.2 属性的操作
5.2.1 属性的读取
有两种方式,用点和方括号(python就是这样)。
但是需要注意,当使用方括号时,键必须被引号引起来,否则就会被当作变量处理。
let a = "ok"
let d = {
a : 1,
ok: 2
}
d[a] // 2
d["a"] // 1
键名是数字可以不加引号,因为可以自动转化为字符串。并且数字键名不能使用点运算符,因为会被当作小数点。
let a = {3:"three"}
a.3 // SyntaxError: Unexpected number
a[3] // "three"
5.2.2 属性的赋值
跟 python 一样,略。
5.2.3 属性的查看
查看所有属性:Object.keys()
let w = {1:2,2:3,3:4}
Object.keys(w) // ["1","2","3"]
python 跟这个不一样,
w = {1:2,2:3,3:4}
w.keys()
造成这样不一样的原因是,在 python 中讲究一切皆对象。代码中的 w
是 python dict 类的一个实例。而 python 定义的dict有一个 keys() 的方法。
而JS中并不是这样做的。而是把 w 当作参数传入进去。
5.2.4 属性的删除:delete 命令
删除属性使用 delete,但是 delete 在删除属性时无论该属性是否存在都会返回 true。
let obj = {}
delete obj.p // true
除非是删除私有属性才会返回 false,并且这样的私有属性不会通过 delete 删除。
let obj = Object.defineProperty({},'p',{value:2})
obj.p // 2
delete obj.p // false
另外需要注意,继承的属性是不能删除的
let obj = {}
delete obj.toString // true
阮大这个思考很深入啊,之前没有想过删除一下从内置的继承属性
del w.keys
// AttributeError: 'dict' object attribute 'keys' is read-only
// keys 只是可读属性。属性分两种,可读和可修改
5.2.5 属性是否存在:in 运算符
基本上可以按照 Python 中的 in 来理解,可以用在对象和数组上。Python 中的 in 作用的对象要求必须是 iterable 。
in 运算符有一个问题,它不能识别哪些属性是对象自身的,哪些属性是继承的。
let arr = []
"toString" in arr // true
这时,可以使用对象的 hasOwnProperty
方法判断一下,是否为对象自身的属性。
let obj = {};
if('toString' in obj){
console.log(obj.hasOwnProperty('toString'));
}
5.2.6 属性的遍历:for ... in 循环
for ... in 循环用来遍历一个对象的所有属性。
for(let i in {a:1,b:2}){
console.log(i)
}
数组本质上也是一种特殊的属性,所以用 for ... in 遍历返回的是索引位置。
for ... in 循环使用时有两个注意点:
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性
- 它不仅遍历对象自身的属性,还遍历继承的属性。
如果只想遍历对象自身的属性,需要结合hasOwnProperty
, 在循环内部判断一下,某个属性是否为对象自身的属性
let person = {name:333}
for(let i in person){
if (person.hasOwnProperty(i)){
console.log(i);
}
}
使用 for ... of 遍历对象会抛异常:
typeError : intermediate value is not iterable
复制值是不可迭代的
这个错误也凸显 for ... of 和 for ... in 真正的区别:
for ... in 是遍历对象;
for ... of 是遍历可迭代对象,这个才是模仿 python 的 for ... in...
5.3 with 语句
with
语句的格式如下:
with (对象){
语句;
}
它的作用是操作同一个对象的多个属性时,提供一些书写方便。
let obj = {
p1 : 1,
p2 : 2,
}
with (obj){
p1 = 4;
p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;
注意,如果 with 区块内部有变量的赋值操作,必须是当前已经存在的属性,否则就会创造一个当前作用域的全局变量。
let obj = {a:1}
with (obj){
a = 8;
b = 99;
}
obj.a // 8
obj.b // undefined
b // 99
with 区块没有改变作用域,它的内部依然是当前作用域。这造成了 with 语句的一个很大弊病,就是绑定对象不明确。
编译器无法在编译时提前优化,因为 它无法判断with 语句中的变量是全局变量还是 obj 的一个属性,只能等到运行时判断,这就拖慢了运行速度。
python 中也有 with,但是 with 是一个上下文管理工具,和 js 中 with 作用完全不一样。
6 函数
教程:https://wangdoc.com/javascript/types/function.html
6.1 概述
6.1.1 函数的声明
JavaScript 有三种声明函数的方法。
(1) 使用 function
(2) 函数表达式
let a = ()=>{}
一般这种形式,函数表达式都是一个匿名函数,但是也可以是一个带有函数名的函数。这时,该函数名只在函数体内部有效,在函数体外部无效。
let print = function x(){
console.log(typeof x);
}
x // ReferenceError: x is not defined
print() // function
这种写法有两种好处:
一是可以在函数体内部调用自身,
二是方便排错,除错工具显示函数调用栈时,将显示函数名,而不再这里是一个匿名函数。
let f = function f() {};
需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。函数的声明在结尾的大括号后面不用加分号。
第三种声明函数的方式是 Function 构造函数
let add = new Function(
'x',
'y',
'return x + y'
)
// 这种写法等同于
function add(x,y){
return x + y
}
可以传递任意数量的参数给 Function
构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
6.1.2 函数的重复声明
如果一个函数被多次重复声明,那么后面的声明就会覆盖前面的声明。
阮大真是脑回路清奇,实验了一下,在 Python 也是如此,后面的声明可以覆盖前面的声明。
但是,由于 JS 函数名的提升(参见下文), 前一次声明在任何时候都是无效的,这一点特别注意。Python 中因为不会像JS 这样先解释在运行,而是边解释边运行,所以还是有效的。
function a(){
console.log("111");
}
a()
function a(){
console.log("222");
}
a()
// 222
// 222
6.1.3 圆括号运算符、return 语句和递归
圆括号运算符就是调用函数。
递归就是递归计算呗
6.1.4 第一等公民
函数是 JS 中的一等公民,也就是说函数是一种值,它与其他值(数值、布尔值、字符串等等)地位相同。凡是可以使用值的地方,就能使用函数。
function add(x, y){
return x + y;
}
// 将函数赋值给一个变量
let op = add;
// 将函数作为参数和返回值
function a(op){
return op;
}
// python 也支持这一系列操作的
// a(add) 就是函数 add 本身了
// (1,1) 表示要调用 add 这个函数
a(add)(1,1)
6.1.5 函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用 function
命令声明函数时,整个函数会像变量声明一样,被提升到代码的头部。所以,下面代码是不会报错的。
f();
function f() {}
但是如果使用赋值语句定义函数,JS 就会报错。
f();
var f = ()=>{};
// TypeError: f is not defined
上面的代码等于
var f ;
f();
f = function () {};
因此,同时采用 function
命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。
6.2 函数的属性和方法
6.2.1 name 属性
函数的name
属性返回函数的名字
function f1(){}
f1.name // f1
如果通过变量赋值定义的函数,name
属性返回变量名。
let f2 = function(){}
f2.name // "f2"
如果变量的值是一个具名函数,那么name
属性返回的是具名函数的函数名。
let f2 = function myFunction(){}
f2.name // "myFunction"
name
属性的应用场景
name
属性的一个用处,就是获取参数函数的名字。
let myFunc = function(){};
function test(f){
console.log(f.name);
}
test(myFunc)
//"myFunc"
6.2.2 length 属性
函数的 length
属性返回函数签名中需要返回的函数个数
function f(a, b){}
f.length // 2
6.2.3 toString()
返回一个字符串,内容为函数的定义,即便是注释部分也会被返回
6.3 函数作用域
6.3.1 定义
作用域(scope)指的是变量存在的范围。在 ES5 中,JS 只有两宗作用域:全局作用域,变量在整个程序中一直存在,所有地方都可以读取;函数作用域,变量只在函数内部存在。ES6 新增了块级作用域。
函数外部声明的变量就是全局作用域,全局作用域可以在函数内部读取。
var a = 1;
function w(){
console.log(a);
}
// 1
函数内部定义的变量就是局部变量,不能在函数外部读取。
function w(){
var a = 1;
}
console.log(a);
// ReferenceError: v is not defined
如果全局变量和局部变量重名,那么在局部作用域内局部变量会覆盖全局变量。这个很好理解。
需要注意的是,var 只有在函数内部定义的才是局部变量,在其他块级作用域内定义的都是全局变量。
if(true){
var a = 3;
}
console.log(a);
// a
这点也是 let 与 var 的区别,let 定义的变量即便在其他的块级作用域内,依旧是局部变量。
if(true){
let a = 3;
}
console.log(a);
// RefereceError: a is not defined
6.3.2 函数内部的变量提升
在函数作用域内部,也会出现变量提升。在函数作用域内部,var 声明的变量不管在什么位置,会提升到函数作用域的头部。
function foo(){
console.log(w);
var w = 100;
}
foo() // undefined
6.3.3 函数本身的作用域
有点莫名其妙,看完之后觉得都是应该的。
6.4 参数
6.4.1 概述
函数的参数就是函数签名中括号中的那些变量。
6.4.2 参数的省略
在 JS 中函数的参数可以省略,这点 JS 独有的。比如在 Python 中如果省略参数会直接抛出异常,在 JAVA 中不同的参数个数还可以区分不同的方法。
function f(a, b){
return a + b;
}
f(2) // NaN
f() // NaN
当前不能省略前面的参数,这点是毫无疑问的
6.4.3 传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),则是值传递。即传入的参数值是原始值的拷贝,在函数内修改参数的值,不会对原始值产生影响。
函数参数如果是容器类型(数组、对象、其他函数等),那么传值的方式是值引用,传进入的是一个地址。如果在函数内修改参数的值,原始值也会随之发生改变。
// Python
def a(x):
x[0] = 33
w = [1, 2]
a(w)
w //[33,2]
但是注意,如果函数内部修改的,不是参数对象的属性,而是替换整个参数,这时不会影响到原始值。
def a(x):
x = [1, 2, 3]
w = ["a", "b"]
a(w)
w
// ["a", "b"]
这是因为,形式参数 x 的值实际是参数 w 的地址,重新对 x 赋值导致 x 指向另一个地址,保存在原地址上的值当然不受影响。
为什么对于原始值是值拷贝,容器类型则是值引用?当然是容器类型东西太多了,为了提高效率才这么做的。
6.4.4 同名参数
这个也算是 JS 特性了。
如果有同名的参数,则取最后出现的那个值。
function f(a, a){
console.log(a);
}
f(1,2)
// 2
取值的时候,以后面的 a
为准,即使后面的 a
没有值或者被省略,也是以其为准。
f(1) // undefined
调用函数 f
的时候,没有提供第二个参数,a
的取值就变成了 undefined
。这时,如果要获得第一个 a
的值,可以使用 arguments
的对象。
function f(a,a){
console.log(arguments[0]);
}
f(1) // 1
6.4.5 arguments 对象
(1) 定义
由于 JS 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是 arguments 对象的由来。
这个特点可以联想到 shell,shell 的参数也可以使用 1 /2 / $3 ... 来表示。arguments[0] 表示第一个参数、arguments[1] 表示第二个参数...
arguments 对象只有在函数体内部才可以使用。
正常情况下,arguments 对象可以在运行时修改
let f = function(a, b){
arguments[0] = 3;
arguments[1] = 2;
return a + b ;
}
f(1, 1) // 5
但是在严格模式下,arguments
对象是一个只读对象,修改它是无效的,但是不会报错
function f(a,b){
"use strict"
arguments[0] = 1;
arguments[1] = 2;
return a + b;
}
f(1,1) // 2
可以通过 arguments
对象的 length
属性,可以判断函数调用时到底带几个参数。
function f(){
return arguments.length;
}
f(1,2,3) //3
f() // 0
(2) 与数组的关系
阮大对 JS arguments 的描述,让我想起了 Python 的不定参数 *args
。
arguments 很像数组,但是它是一个对象。数组专有的方法(比如 slice
和 forEach
),不能在 arguments
上直接使用。
如果想让 arguments
对象使用数组方法,真正的解决方法是将 arguments
转化为真正的数组。转换方法有两种:
// 方法一
var args = Array.prototype.slice.call(arguments);
// 方法二
var args = [];
for(var i = 0; i < arguments.length; i++){
args.push(arguments[i]);
}
(3) callee 属性
callee属性返回它所对应的原函数。
这个属性在严格模式下是禁止使用的。
6.5 函数的其他知识点
6.5.1 闭包
理解闭包(closure),首先要理解作用域。JS 有两种作用域:全局作用域和函数作用域。
函数内部可以直接读取全局变量。
函数外部不可以读取函数内部声明的局部变量。
如果想在函数外部读取函数内部定义的局部变量,就需要使用闭包。
function f1(){
var n = 999;
function f2(){
console.log(n);
}
f2(); // 999
}
f2 在 f1 的内部,f1 内部所有的变量对于 f2 来说都是可见的。反过来,当然不成立。
既然 f2
可以读取 f1
的局部变量,那么只要把f2
当作返回值,就可以在f1 函数的外部读取 f1 内部的变量了。
function f1(){
var n = 999;
function f2(){
return n;
}
return f2;
}
f1()() // 999
函数 f2
就是闭包,即能够读取其他函数内部变量的函数。由于在 JS 语言中,只有函数内部的子函数能够读取内部变量,因此可以把闭包简单理解为“定义在函数内部的函数”。
闭包有两个作用:
- 可以读取函数内部的变量;
- 让这些变量始终保持在内存中,即闭包可以使得它的诞生环境一直存在。
function createIncrementor(start){
return function (){
return start++;
}
}
// i++ 第一次是加0,不知道是为什么
var inc = createIncrementor(5);
inc(); // 5
inc(); // 6
闭包 inc
使得函数 createIncrementor
的内部环境一直存在。原因在于,inc
始终在内存中,而 inc
的存在依赖于 createIncrementor
,因此也始终在内存中,不会在调用结束后,被垃圾回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
function Person(name){
var _age;
function setAge(n){
_age = n;
}
function getAge(){
return _age;
}
return {
name: name,
setAge: setAge,
getAge: getAge,
}
}
var p = Person("LiMing");
p.setAge(5);
p.getAge(); // 5
注意,外层函数每次运行,都会生成一个新的闭包,这个闭包又会保留外层函数的内部变量,所以内存消耗很大。换成面向对象的语言来说,每次调用 Person对象 都会生成一个新的实例,这些实例有自己的属性,生成的实例多了当然会占内存。
6.5.2 立即调用的函数表达式(IIFE)
在 JS 中,圆括号()
是一种运算符,跟在函数名后面表示函数立即调用。
但是需要注意,定义函数之后,不能这样立即调用函数。
function() { /* code */}();
// SyntaxError: Unexpected token (
产生这个错误的原因,function
这个关键字即可以当作语句,又可以当作表达式。
// 语句
function f() {}
// 表达式
var f = function f() {}
为了避免解析上的歧义,JS 引擎规定,如果 function
关键字出现在行首,一律解析成语句。因此,JS 引擎看到行首是 function
关键字之后,认为这一段是函数的定义,不应该以圆括号结尾,就报错了。
解决办法是不要让function
出现在行首,让引擎将其理解为一个表达式。最简单的处理办法是,将函数体放到一个圆括号里。
(function(){}());
// 或者
(function(){})();
这种就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能会报错。
(function(){}())
(function(){})()
// TypeError: (intermediate value)(...) is not a function
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法:
var i = function(){}();
true && function(){}();
0, function(){}();
甚至像下面这样写,也是可以的:
!function(){}();
~function(){}();
+function(){}();
对匿名函数使用这种 “立即执行的函数表达式”,有两个目的:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function(){
var tmp = newData;
processData(tmp);
storeData(tmp);
})();
6.6 eval 命令
6.6.1 基本用法
eval 接受一个字符串当作参数,并将这个字符串当作语句执行。
eval('var a = 1');
a // 1
如果参数字符串无法当作语句执行,就会报错。
eval 没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
var a = 1;
eval('a = 3');
a
// 3
为了避免这种风险,JS 规定,如果使用严格模式, eval 内部声明的变量,不会影响到外部作用域。
(function(){
'use strict';
eval('var foo = 123;');
console.log(foo)
}())
// ReferenceError: foo is not defined
不过即使在严格模式下, eval
依然可以读写当前作用域的变量。
(function f(){
'use strict';
var foo =1;
eval('foo = 2');
console.log(foo); // 2
})()
总之,eval
的本质是在当前作用域之中,注入代码。eval 不利于引擎优化。
6.6.2 eval 的别名调用
引擎在静态代码分析的阶段,根本无法分辨执行的是 eval
var m = eval;
m("var x =1 ");
x // 1
上面的代码中,变量 m
是 eval
的别名。静态代码分析阶段,引擎分辨不出m('var x=1 ')
执行的 eval
命令。
为了保证 eval
的别名不影响代码优化,JS 标准规定,凡是使用别名执行eval
,eval
内部一律是全局作用域。
var a =1;
function f(){
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
引擎只能识别 eval()
,eval 的别名调用都属于别名调用。
eval.call(null, '...')
window.eval('...')
(1,eval)('...')
(eval,eval)('...')
7 数组
教程地址:https://wangdoc.com/javascript/types/array.html
7.1 定义
跟 python 一样
7.2 数组的本质
数组的本质是特殊的对象。typeof
运算符会返回数组的类型是 object
。
typeof [1,2,3] // "object"
数组的键名是按次序排列的一组整数。
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]
由于数组成员的键名总是固定的,因此数组不用为每个元素指定键名。JS 语言规定,对象的键名一律为字符串,所以,数组的键名是字符串。之所以可以用数值读取,是因为非字符串的键名会被转化为字符串。
var arr = ['a', 'b', 'c'];
arr[0] // 'a'
arr['0'] // 'a'
所以,arr 添加新元素除了 push 之后,还可以这样子
let arr = [];
a[3] = 888
arr
// [undefined, undefined, undefined, 888]
这个操作,Python 没有。
7.3 length 属性
arr.length
// 返回数组的长度
length 属性可以读写。如果人为把 length 减少到小于数组的长度,那么数组内元素的个数也会减少。
let arr = [1,2,3,4];
arr.length = 2;
arr // [1,2]
清空数组,可以把数组的长度置为0.
arr.length = 0
如果把length 的长度增大,那么数组内多出来的是空位. 空位和 元素是 undefined 不是一个概念。
Python 数组的长度当然不能这么改变。因为 len 是函数调用。
7.4 in 运算符
in 是用来检查键名是否存在于对象,这个也适用于数组。
要注意,检查的是数组的键名,也就是索引值。
let arr = ["a", "b", "c"]
1 in arr // true
"a" in arr // false
7.5 for...in 循环和数组的遍历
for ... in 切莫当成 Python 的 for...in 。
JS 的 for ... in 索引的是键名。
let arr = ["a", "b", "c"];
for(let i in arr){
console.log(i);
}
// 0 1 2
此外,数组不仅会遍历数组所有的数字键,还会遍历非数字键。
var a = [1, 2, 3];
a.foo = true
for(let key in a){
console.log(key);
}
// 0 1 2 foo
遍历数组一般可以使用 for
循环或者 while
循环。
也可以使用 forEach
7.6 数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称数组之间存在空位(hole)。
let a = [1,,3]
a.length // 3
空位不影响 length 属性。
空位是可以读取的,返回 undefined
var a = [,,,];
a[0] // undefined
使用 delete 删除一个数组成员,会形成空位,并且不会影响 length 属性。
var a = [1,2,3];
delete a[1];
a[1] // undefined
a.length // 3
delete 删除了数组的第二个元素,这个位置就是空位,对 length
属性没有影响。
数组的某个位置是空位,与某个位置是 undefined
,是不一样的。如果是空位,使用数组的 forEach
,for .. in , Object.keys 方法进行遍历,空位都会被跳过。
var a = [,,,];
空位就是数组没有这个元素,所以不会遍历到,而 undefined
则表示数组有这个元素,值是 undefined
,所以遍历不会跳过。
7.7 类似数组的对象
如果一个对象的所有键名都是正整数或0,并且有 length 属性,那么这个对象就很像数组,语法上称为 “类似数组的对象”。
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
obj[0] // 'a'
obj.length // 3
类似数组的对象,当然不能使用 push 方法了,当然不能使用 length 删减元素了。
典型的 “类似数组的对象”是函数的arguments
对象,以及大多数 DOM 元素集,还有字符串。
// arguments 对象
function args(){
return arguments
}
let arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM 元素集
let ele = document.getElementsByTagName('h');
ele.length // 0
ele[0] // undefined
// 字符串
let str = "abdc"
str.length // 4
str instanceof Array // false
数组的 slice
方法可以将“类似数组的对象”变成真正的数组。
let arr = Array.prototype.slice.call(arrayLike)'
还有一个办法,通过call
把数组的方法放到对象上面。
let arrLike = {
0: 1,
1:22,
2:33,
length:3,
}
function print(value, index){
console.log(index + ":" + value);
}
Array.prototype.forEach.call(arrayLike, print);
arrayLike 是个类数组的对象,本来不可以使用数组的 forEach() 方法的,但是通过 call(),可以把 forEach()
嫁接到 arrayLike 上面调用。
Python 也是有类似行为的,一个类如果加上某些特定的双下划线方法就可以使用一些 Python 方法。
字符串也是类似数组的对象,所以也可以使用 Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr){
console.log(chr);
});
// a b c
这种方法比直接调用数组原生的 forEach
要慢,所以最好还是将类似数组的对象转为真正的数组,然后再调用数组的 forEach
方法。
let arr = Array.prototype.slice.call('abc');
arr.forEach((chr)=>{
console.log(chr);
});