1. 七种数据类型
数值(number):整数和小数(比如1和3.14)字符串(string):文本(比如Hello World)布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值null:表示空值,即此处的值为空对象(object):各种值组成的集合symbol
- 基本数据类型:number、string、bookean、symbol
- 空类型:undefinged、null
- 容器数据类型:object
- 狭义的对象(object)
- 数组(array)
- 函数(function)
查看值的类型
// typeof 用法typeof 123 // "number"typeof '123' // "string"typeof false // "boolean"function f() {}typeof f // "function"typeof undefined // "undefined"typeof window // "object"typeof {} // "object"typeof [] // "object"typeof null // "object" // null的类型是object,这是由于历史原因造成的// typeof 在判断语句下的使用if (typeof v === "undefined") {// ...}// instanceof 用法var o = {};var a = [];o instanceof Array // falsea instanceof Array // true
2. null,undefined 和 布尔值
null与undefined都可以表示“没有”,含义非常相似
// 语法效果几乎没区别// 同时设置两个这样的值与历史原因有关var a = undefined;var a = null;// null 转数字Number(null) // 05 + null // 5// undefined 转数字Number(undefined) // NaN5 + undefined // NaN// 这样理解 null 和 defined 用法和含义// null表示空值,即该处的值现在为空// undefined表示“未定义”,下面是返回undefined的典型场景var i; // 变量声明了,但没有赋值i // undefinedfunction f(x) { // 调用函数时,应该提供的参数没有提供,该参数等于 undefinedreturn x;}f() // undefinedvar o = new Object(); // // 对象没有赋值的属性o.p // 函数没有返回值时,默认返回 undefinedfunction f() {}f() // undefined
布尔值
- “真”用关键字true表示,“假”用关键字false表示
- 如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值
// 5 个会被转为 false 的值undefinednullfalse0NaN'' 或 ""// 注意,空数组([])和空对象({})对应的布尔值,都是true
3. 数值
- JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此
- 由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心
- 与数值相关的全局方法
- parseInt()
- parseFloat()
- isNaN()
- isFinite()
Math.pow(2, 53) + 2 // 9007199254740994Math.pow(2, 53) + 3 // 9007199254740996Math.pow(2, 53) + 4 // 9007199254740996// 大于2的53次方以后,多出来的有效数字(最后三位的111)都会无法保存,变成0// 所以简单的法则就是,JavaScript 对15位的十进制数都可以精确处理Math.pow(2, 1024) // Infinity// 如果一个数大于等于2的1024次方,那么就会发生“正向溢出”Math.pow(2, -1075) // 0// 如果一个数小于等于2的-1075次方,那么就会发生为“负向溢出”// JavaScript 提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值。Number.MAX_VALUE // 1.7976931348623157e+308Number.MIN_VALUE // 5e-324
数值的进制
// 默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制0xff // 2550o377 // 2550b11 // 30xzz // 报错0o88 // 报错0b22 // 报错
正零和负零
-0 === +0 // true0 === -0 // true0 === +0 // true+0 // 0-0 // 0(-0).toString() // '0'(+0).toString() // '0'(1 / +0) === (1 / -0) // false// 因为除以正零得到+Infinity,除以负零得到-Infinity
NaN
- 主要出现在将字符串解析成数字出错的场合
5 - 'x' // NaNMath.acos(2) // NaNMath.log(-1) // NaNMath.sqrt(-1) // NaN0 / 0 // NaNtypeof NaN // 'number'NaN === NaN // falseBoolean(NaN) // falseNaN + 32 // NaNNaN - 32 // NaNNaN * 32 // NaNNaN / 32 // NaN
parseInt()
parseInt方法用于将字符串转为整数parseInt('123') // 123// 如果字符串头部有空格,空格会被自动去除parseInt(' 81') // 81// 如果parseInt的参数不是字符串,则会先转为字符串再转换parseInt(1.23) // 1// 等同于parseInt('1.23') // 1// parseInt的参数都是字符串,结果只返回字符串头部可以转为数字的部分parseInt('8a') // 8parseInt('12**') // 12parseInt('12.34') // 12parseInt('15e2') // 15parseInt('15px') // 15// 如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaNparseInt('abc') // NaNparseInt('.3') // NaNparseInt('') // NaNparseInt('+') // NaNparseInt('+1') // 1// 如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析// 如果字符串以0开头,将其按照10进制解析parseInt('0x10') // 16// 进制转换parseInt('1000') // 1000parseInt('1000', 2) // 8parseInt('1000', 6) // 216parseInt('1000', 8) // 512// 第二个参数表示被解析的值的进制// 第二个参数只有在2到36之间,超出这个范围,则返回NaN// 如果第二个参数是0、undefined和null,则直接忽略parseInt('10', 37) // NaNparseInt('10', 1) // NaNparseInt('10', 0) // 10parseInt('10', null) // 10parseInt('10', undefined) // 10
parseFloat()
parseFloat('3.14') // 3.14parseFloat('314e-2') // 3.14parseFloat('0.0314E+2') // 3.14parseFloat('3.14more non-digit characters') // 3.14parseFloat([]) // NaNparseFloat('FF2') // NaNparseFloat('') // NaN// 尤其值得注意,parseFloat会将空字符串转为NaN
isNaN()
- 使用isNaN之前,最好判断一下数据类型
// isNaN只对数值有效,如果传入其他值,会被先转成数值isNaN(NaN) // trueisNaN(123) // falseisNaN('Hello') // trueisNaN({}) // trueisNaN(['xzy']) // true// 对于空数组和只有一个数值成员的数组,isNaN返回false// 原因是这些数组能被Number函数转成数值isNaN([]) // falseisNaN([123]) // falseisNaN(['123']) // false
isFinite()
- 返回一个布尔值,表示某个值是否为正常的数值
isFinite(Infinity) // falseisFinite(-Infinity) // falseisFinite(NaN) // falseisFinite(undefined) // falseisFinite(null) // trueisFinite(-1) // true// 除了Infinity、-Infinity、NaN和undefined这几个值会返回false// isFinite对于其他的数值都会返回true。
4. 字符串
// 合法字符串'abc'"abc"'key = "value"'"It's a long journey"'Did she say \'Hello\'?'"Did she say \"Hello\"?"// 分多行会报错'abc'// // SyntaxError: Unexpected token ILLEGAL// 若要分行,每一行尾部加 \var longString = 'Long \long \long \string';longString// "Long long long string"// 连接运算符(+)可以连接多个单行字符串var longString = 'Long '+ 'long '+ 'long '+ 'string';// 转义\0 :null(\u0000)\b :后退键(\u0008)\f :换页符(\u000C)\n :换行符(\u000A)\r :回车键(\u000D)\t :制表符(\u0009)\v :垂直制表符(\u000B)\' :单引号(\u0027)\" :双引号(\u0022)\\ :反斜杠(\u005C)
字符串可以被视为字符数组,因此可以使用数组的方括号运算符
var s = 'hello';s[0] // "h"s[1] // "e"s[4] // "o"
length 属性
// length 属性无法改变var s = 'hello';s.length // 5s.length = 3;s.length // 5s.length = 7;s.length // 5
Base64 转码
- 主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理
// btoa():任意值转为 Base64 编码// atob():Base64 编码转为原来的值var string = 'Hello World!';btoa(string) // "SGVsbG8gV29ybGQh"atob('SGVsbG8gV29ybGQh') // "Hello World!"// 这两个方法不适合非 ASCII 码的字符,会报错btoa('你好') // 报错// 要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节function b64Encode(str) {return btoa(encodeURIComponent(str));}function b64Decode(str) {return decodeURIComponent(atob(str));}b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
5. 对象
- 一组“键值对”(key-value)的集合
- 对象的所有键名都是字符串,如果键名是数值,会被自动转为字符串
var obj = {foo: 'Hello',bar: 'World'};// 如果属性的值还是一个对象,就形成了链式引用var o1 = {};var o2 = { bar: 'hello' };o1.foo = o2;o1.foo.bar // "hello"
表达式还是语句?
// 歧义的代码// 如果是一个表达式,表示一个包含foo属性的对象// 如果是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123{ foo: 123 }// JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块// 如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式({ foo: 123 }) // 正确({ console.log(123) }) // 报错
属性的读取
// 点运算符和方括号运算符var obj = {p: 'Hello World'};obj.p // "Hello World"obj['p'] // "Hello World"// 方括号里用变量var foo = 'bar';var obj = {foo: 1,bar: 2};obj.foo // 1obj[foo] // 2// 方括号运算符内部还可以使用表达式obj['hello' + ' world']obj[3 + 3]// 数字键可以不加引号,因为会自动转成字符串var obj = {0.7: 'Hello World'};obj['0.7'] // "Hello World"obj[0.7] // "Hello World"
属性的赋值
var obj = {};obj.foo = 'Hello';obj['bar'] = 'World';var obj = { p: 1 };// 等价于var obj = {};obj.p = 1;
属性的查看
// 返回一个数组var obj = {key1: 1,key2: 2};Object.keys(obj);// ['key1', 'key2']
属性的删除:delete 命令
// delete命令用于删除对象的属性,删除成功后返回 truevar obj = { p: 1 };Object.keys(obj) // ["p"]delete obj.p // trueobj.p // undefinedObject.keys(obj) // []// 注意,删除一个不存在的属性,delete不报错,而且返回truevar obj = {};delete obj.p // true
属性是否存在:in 运算符
// in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的var obj = { p: 1 };'p' in obj // true'toString' in obj // true// 对象的hasOwnProperty方法判断一下,是否为对象自身的属性var obj = {};if ('toString' in obj) {console.log(obj.hasOwnProperty('toString')) // false}
属性的遍历:for…in 循环
var obj = {a: 1, b: 2, c: 3};for (var i in obj) {console.log('键名:', i);console.log('键值:', obj[i]);}// 使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性var person = { name: '老张' };for (var key in person) {if (person.hasOwnProperty(key)) {console.log(key);}}
with 语句
- 操作同一个对象的多个属性时,提供一些书写的方便 ```javascript // 例一 var obj = { p1: 1, p2: 2, }; with (obj) { p1 = 4; p2 = 5; } // 等同于 obj.p1 = 4; obj.p2 = 5;
// 例二 with (document.links[0]){ console.log(href); console.log(title); console.log(style); } // 等同于 console.log(document.links[0].href); console.log(document.links[0].title); console.log(document.links[0].style);
// 注意,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性 // 否则会创造一个当前作用域的全局变量
<a name="1FdR9"></a>### 6. 函数函数声明```javascript// function 命令function print(s) {console.log(s);}// 函数表达式var print = function(s) {console.log(s);};// Function 构造函数var add = new Function('x','y','return x + y');// 等同于function add(x, y) {return x + y;}// 以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体// 如果只有一个参数,该参数就是函数体
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明
function f() {console.log(1);}f() // 2function f() {console.log(2);}f() // 2
圆括号运算符,return 语句和递归
- 函数体内部的return语句,表示返回
- JavaScript 引擎遇到return语句,就直接返回return后面的那个表达式的值
- 后面即使还有语句,也不会得到执行
- 也就是说,return语句所带的那个表达式,就是函数的返回值
- return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined
function add(x, y) {return x + y;}add(1, 1) // 2// 函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码function fib(num) {if (num === 0) return 0;if (num === 1) return 1;return fib(num - 2) + fib(num - 1);}fib(6) // 8
第一等公民
- JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同
函数名的提升
- JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时
- 整个函数会像变量声明一样,被提升到代码头部
f();function f() {}// 采用赋值语句定义函数,JavaScript 就会报错f();var f = function (){};// TypeError: undefined is not a function// 用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义var f = function () {console.log('1');}function f() {console.log('2');}f() // 1
函数的属性和方法
// name 属性function f1() {}f1.name // "f1"length 属性,函数预期传入的参数个数function f(a, b) {}f.length // 2// toString()方法返回一个字符串,内容是函数的源码function f() {a();b();c();}f.toString()// function f() {// a();// b();// c();// }// 对于那些原生的函数,toString()方法返回function (){[native code]}Math.sqrt.toString() // "function sqrt() { [native code] }"// 函数内部的注释也可以返回function f() {/*这是一个多行注释*/}f.toString()// "function f(){/*// 这是一个// 多行注释// */}"// 利用这一点,可以变相实现多行字符串var multiline = function (fn) {var arr = fn.toString().split('\n');return arr.slice(1, arr.length - 1).join('\n');};function f() {/*这是一个多行注释*/}multiline(f);// " 这是一个// 多行注释"
函数作用域
- ES5 中,JavaScript 只有两种作用域,全局作用域和函数作用域
- ES6 新增了块级作用域
- 对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取
- 在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)
- 对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量
var v = 1;function f() {console.log(v);}f()// 1
函数内部的变量提升
- 与全局作用域一样,函数作用域内部也会产生“变量提升”现象
- var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function foo(x) {if (x > 100) {var tmp = x - 100;}}// 等同于function foo(x) {var tmp;if (x > 100) {tmp = x - 100;};}
函数本身的作用域
- 函数本身也是一个值,也有自己的作用域。它的作用域与变量一样
- 就是其声明时所在的作用域,与其运行时所在的作用域无关
// 函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值var a = 1;var x = function () {console.log(a);};function f() {var a = 2;x();}f() // 1// 函数x是在函数y体外声明的,作用域绑定外层,因此找不到函数y的内部变量a,导致报错var x = function () {console.log(a);};function y(f) {var a = 2;f();}y(x)// ReferenceError: a is not defined// 同样地,函数体内部声明的函数,作用域绑定函数体内部function foo() {var x = 1;function bar() {console.log(x);}return bar;}var x = 2;var f = foo();f() // 1// 函数foo内部声明了一个函数bar,bar的作用域绑定foo// 当我们在foo外部取出bar执行时,变量x指向的是foo内部的x,而不是foo外部的x// 正是这种机制,构成了“闭包”现象
参数
- 函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数
// 参数允许省略function f(a, b) {return a;}f(1, 2, 3) // 1f(1) // 1f() // undefinedf.length // 2// 函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数
参数传递
- 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)
- 这意味着,在函数体内修改参数值,不会影响到函数外部
- 如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)
- 也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值
- 如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值
// 在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值var p = 2;function f(p) {p = 3;}f(p);p // 2// 在函数内部修改obj的属性p,会影响到原始值var obj = { p: 1 };function f(o) {o.p = 2;}f(obj);obj.p // 2// 在函数f()内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值// 这是因为,形式参数(o)的值实际是参数obj的地址// 重新对o赋值导致o指向另一个地址,保存在原地址上的值当然不受影响var obj = [1, 2, 3];function f(o) {o = [2, 3, 4];}f(obj);obj // [1, 2, 3]
同名参数
- 如果有同名的参数,则取最后出现的那个值
function f(a, a) {console.log(a);}f(1, 2) // 2function f(a, a) {console.log(a);}f(1) // undefinedfunction f(a, a) {console.log(arguments[0]);}f(1) // 1
arguments 对象
- 可以在函数体内部读取所有参数
- arguments对象包含了函数运行时的所有参数
- arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推
- 这个对象只有在函数体内部,才可以使用
var f = function (one) {console.log(arguments[0]);console.log(arguments[1]);console.log(arguments[2]);}f(1, 2, 3)// 1// 2// 3// 正常模式下,arguments对象可以在运行时修改var f = function(a, b) {arguments[0] = 3;arguments[1] = 2;return a + b;}f(1, 1) // 5严格模式下,arguments对象与函数参数不具有联动关系var f = function(a, b) {'use strict'; // 开启严格模式arguments[0] = 3;arguments[1] = 2;return a + b;}f(1, 1) // 2
arguments 与 数组的关系
- 虽然arguments很像数组,但它是一个对象
- 数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用
var args = Array.prototype.slice.call(arguments);// 或者var args = [];for (var i = 0; i < arguments.length; i++) {args.push(arguments[i]);}
闭包
- 闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现
- 得到函数内的局部变量的方法,在函数的内部,再定义一个函数
- 闭包的最大用处有两个
- 一个是可以读取函数内部的变量
- 另一个就是让这些变量始终保持在内存中
- 闭包的另一个用处,是封装对象的私有属性和私有方法
- 外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量
- 所以内存消耗很大,因此不能滥用闭包,否则会造成网页的性能问题 ```javascript // 闭包就是函数f2,即能够读取其他函数内部变量的函数 function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
// 闭包使得内部变量记住上一次调用时的运算结果 function createIncrementor(start){ return function(){ return start++ } } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7 // start是函数createIncrementor的内部变量 // 通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算
function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; }
// 函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量 var p1 = Person(‘张三’); p1.setAge(25); p1.getAge() // 25
立即调用的函数表达式- function 这个关键字即可以当作语句,也可以当作表达式- 为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句- 引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面```javascriptfunction(){ /* code */ }();// SyntaxError: Unexpected token (// 语句function f() {}// 表达式var f = function f() {}// 引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误(function(){ /* code */ }());// 或者(function(){ /* code */ })();// 这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE// 上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错// 如果没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数// 推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果var i = function(){ return 10; }();true && function(){ /* code */ }();0, function(){ /* code */ }();!function () { /* code */ }();~function () { /* code */ }();-function () { /* code */ }();+function () { /* code */ }();// 通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:// 一是不必为函数命名,避免了污染全局变量// 二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量(function () {var tmp = newData;processData(tmp);storeData(tmp);}());
eval 命令
- eval命令接受一个字符串作为参数,并将这个字符串当作语句执行
- eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题
- 不推荐使用
eval('var a = 1;');a // 1eval('3x') // Uncaught SyntaxError: Invalid or unexpected token// eval命令修改了外部变量a的值var a = 1;eval('a = 2');a // 2
7. 数组
- 数组的本质属于一种特殊的对象。typeof运算符会返回数组的类型是object
- 数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)
length 属性是一个动态的值,等于键名中的最大整数加上1['a', 'b', 'c'].length // 3var arr = ['a', 'b'];arr.length // 2arr[2] = 'c';arr.length // 3arr[9] = 'd';arr.length // 10arr[1000] = 'e';arr.length // 1001// 数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1// 如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length设置的值var arr = [ 'a', 'b', 'c' ];arr.length // 3arr.length = 2;arr // ["a", "b"]// 清空数组的一个有效方法,就是将length属性设为0var arr = [ 'a', 'b', 'c' ];arr.length = 0;arr // []var a = ['a'];a.length = 3;a[1] // undefined
in 运算符
var arr = [ 'a', 'b', 'c' ];2 in arr // true'2' in arr // true4 in arr // falsevar arr = [];arr[100] = 'a';100 in arr // true1 in arr // false
for…in 循环和数组的遍历
// for..invar a = [1, 2, 3];for (var i in a) {console.log(a[i]);}// for循环for(var i = 0; i < a.length; i++) {console.log(a[i]);}// while循环var i = 0;while (i < a.length) {console.log(a[i]);i++;}// while逆向遍历var l = a.length;while (l--) {console.log(a[l]);}// forEach 方法遍历数组var colors = ['red', 'green', 'blue'];colors.forEach(function (color) {console.log(color);});// red// green// blue
数组的空位
var a = [1, , 1];a.length // 3var a = [1, 2, 3,];a.length // 3a // [1, 2, 3]var a = [, , ,];a[1] // undefinedvar a = [1, 2, 3];delete a[1];a[1] // undefineda.length // 3// length属性不过滤空位// 空位和undefined,是不一样的// 数组的forEach方法、for...in结构、以及Object.keys方法进行遍历,空位都会被跳过var a = [, , ,];a.forEach(function (x, i) {console.log(i + '. ' + x);}) // 不产生任何输出for (var i in a) {console.log(i);} // 不产生任何输出Object.keys(a) // []// 如果某个位置是undefined,遍历的时候就不会被跳过var a = [undefined, undefined, undefined];a.forEach(function (x, i) {console.log(i + '. ' + x);});// 0. undefined// 1. undefined// 2. undefinedfor (var i in a) {console.log(i);}// 0// 1// 2Object.keys(a)// ['0', '1', '2']// 空位就是数组没有这个元素,所以不会被遍历到// 而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过
类似数组的对象
- 键名都是正整数或零,并且有length属性
// 典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串// arguments对象function args() { return arguments }var arrayLike = args('a', 'b');arrayLike[0] // 'a'arrayLike.length // 2arrayLike instanceof Array // false// DOM元素集var elts = document.getElementsByTagName('h3');elts.length // 3elts instanceof Array // false// 字符串'abc'[1] // 'b''abc'.length // 3'abc' instanceof Array // false// 调用数组的 slice 方法可以将“类似数组的对象”变成真正的数组// 即调用它的截取片段var arr = Array.prototype.slice.call(arrayLike);// 调用数组的 forEach 方法Array.prototype.forEach.call(arrayLike, print);// 这种方法比直接使用数组原生的forEach要慢// 所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法
「@浪里淘沙的小法师」
