三 数据类型

2018-07-27  本文已影响0人  智勇双全的小六

教程

https://wangdoc.com/javascript/types/general.html

1 概述

1.1 数据类型分类

number , string & bool 合成为原始类型(primitive type),因为它们是最基本的数据类型,不能再细分了。
对象是合成类型(complex type),因为一个对象往往由多个原始类型的值合成,是一个存放各种值的容器。
undefined 和 nulll,一般是将它们看为两个特殊值。

对象是最复杂的数据类型,又可以分为三个子类型:

狭义的对象和数组是两种不同的数据组合方式,教程中的对象都是指的是狭义的对象。

函数其实是处理数据的方法,JavaScript 把它当做一种数据类型,可以赋值给变量,这给编程带来灵活性,也为 JS 的函数式编程 奠定基础。Python 也可以把函数赋值给变量。Go 也是支持把函数赋值给变量的。

1.2 typeof 运算符

JavaScript 有三种方法,确定一个值到底是什么类型

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

let a = undefined;
let b =  null;

使用相等运算符,会直接报告它们相等

undefined == null // true

它们的布尔值都是 false

Boolean(null) // false
Boolean(undefined) // false
Number(null) // 0
5 + null // 5
Number(undefined)  // NaN
  1. 变量声明了,但是没有赋值
var i;
i // undefined
  1. 调用函数时,应该提供的参数没有提供,该参数等于 undefined; 这一点算得上是拙劣的设计了,如果是其他的语言会直接抛出异常。
function f(x){
  return x;
}
f() // undefined
  1. 对象没有赋值的属性
var o = new Object();
o.p // undefined
  1. 函数没有返回值,默认返回 undefined
function f() {}
f() // undefined

2.1 布尔值

Python 和 JS 都会对一些值进行隐式的布尔值转换。Go 和 Java 这种强类型语言则不会

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
  1. 小数点后面的零多于5个。
// 小数点后紧跟 5 个以上的零,
// 就自动转化为科学计数法
0.0000003 // 3e-7

3.3 数值的进制

使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。

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

InfinityNaN 比较,总是返回 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

如果字符串以 0x0X 开头,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,这一点要特别引起注意。也就是说,isNaNtrue 的值,有可能不是 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 / NaNundefined 这个值会返回 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 转义

反斜杠 () 在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些:

反斜杠还有三种特殊用法。

  1. \HHH
    反斜杠后面紧跟三个八进制数(000377),代表一个字符。HHH 对应该字符的 Unicode 码点,比如 \251 表示版权符号。显然,这种方法只能输出 256 种字符。
  2. \xHH
    \x 后面紧跟两个十六进制数(00FF),代表一个字符。HH 对应该字符的 Unicode 码点,比如 \xA9 表示版权符号。
  3. \uXXXX
    \u 后面紧跟四个十六进制数(0000FFFF),代表一个字符。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+0000U+FFFF之间的字符,长度为16位(2个字节);对于码点在 U+10000U+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 相关的方法。

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 循环使用时有两个注意点:

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 很像数组,但是它是一个对象。数组专有的方法(比如 sliceforEach),不能在 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

上面的代码中,变量 meval 的别名。静态代码分析阶段,引擎分辨不出m('var x=1 ') 执行的 eval 命令。
为了保证 eval 的别名不影响代码优化,JS 标准规定,凡是使用别名执行evaleval 内部一律是全局作用域。

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);
});
上一篇 下一篇

猜你喜欢

热点阅读