第一篇讲了基本语法以及基本数据类型,第二篇开始讲解复杂数据类型——对象、函数、数组。
对象
定义
这个是要跟之前移动端的概念要区分的地方,移动端的对象值的是一个类alloc(new)出来的一个实体,譬如说一个Person类new一个对象 p,那么p就是一个对象。一个NSDictionary类,可以创建一个字典实体。
但是,在JS中,对象指的是一组“键值对”的集合
,是一种无序的复合数据集合。这种数据结果其实对应iOS中的字典、JAVA中的map,所以在js中,说到对象时,一定一定不要联想到Person类(本质是结构体),其实就是一个键值对、一个字典、一个map而已。
键名
- 键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名)
- 如果键名不是字符串,则会自动转为字符串,如果转换失败,是会报错的。譬如说:
1p
、h w
、p+q
这类非常规变量 - 对象中的键名也称为
属性
,属性的引用通过点语法
,如果属性值为对象,则构成了链式调用 -
键值
可以是任意数据类型。如果一个属性的值为函数,通常称之为“方法”,可以像函数那样调用
对象的引用
多个变量指向同一个对象时
- 修改其中一个变量,会影响到其他所有变量
- 移除某一个变量,对原对象以及其他变量都不会造成影响
函数
{ foo: 123 }
为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错
例如 eval 语句:对字符串求值
- 如果没有圆括号,则理解为一个代码块
- 如果有圆括号,则理解成一个对象
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
属性的读取
- 两种方式:
点运算符
、方括号运算符
- 点运算符后面不能跟数字,因为会被当做小数点处理。例如:
obj.123
这种情况下只能用方括号obj[123]
- 方括号运算符的键名必须放在引号里面,否则会当做变量处理(如果是数字类型,可以不用引号)
属性的赋值
- 同样有两种方式:
点运算符
、方括号运算符
- js中允许
后绑定
,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。
属性的查看: Object.keys
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
属性的删除:delete
-
delete
命令用于删除对象的属性,删除成功后返回true
- 删除一个不存在的属性,
delete
不报错,而且返回true
-
delete
命令只能删除对象本身的属性,无法删除继承的属性(无法删除父类的属性)
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
- 只有通过
Object.defineProperty
声明的属性,在删除的时候返回 false,表示该属性是不能删除的
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false
属性的遍历:for...in
- 它遍历的是对象所有可遍(enumerable)的属性,会
跳过不可遍历的属性
- 它不仅遍历对象自身的属性,还遍历继承的属性
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('键名:', i);
console.log('键值:', obj[i]);
}
// 键名: a
// 键值: 1
// 键名: b
// 键值: 2
// 键名: c
// 键值: 3
- 结合
hasOwnProperty
,在循环内部判断是否是自身的属性
var person = { name: '老张' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
with 语句
- 操作同一个对象的多个属性时,提供书写的方便
var obj = {
p1: 1,
p2: 2,
};
with (obj) {
p1 = 4;
p2 = 5;
}
// 等同于
obj.p1 = 4;
obj.p2 = 5;
-
with
区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创建一个当前作用域的全局变量
var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}
obj.p1 // undefined
p1 // 4
- 【总结】尽量不要使用
with
操作,他会将变量的决议放到运行时判断,这样会拖慢运行速度。下面是一个使用临时变量替换with
的案例:
with(obj1.obj2.obj3) {
console.log(p1 + p2);
}
// 可以写成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);
函数
函数是一段可以反复代用的代码块。函数还能接收输入的参数,不同的参数会返回不同的值。
函数的声明
1、function 命令
格式:function 函数名(参数列表) {...}
function print(s) {
console.log(s);
}
在其他地方调用 print()
就可以调用上述代码了
2、函数表达式
var print = function(s) {
console.log(s);
};
print
表示是左边匿名函数的一个变量,可以直接调用 print()
。
也可以写成第一种样式,带函数名的格式,但是这个函数名只能在函数体里面使用
var print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function
- = 左边的 print 课可以表示是这个函数的一个函数指针
- = 右边的 x 函数名,只能在这个函数体里面使用,如果在其他地方使用,则会报错。
3、构造函数
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
- 可以传递任意多个参数给 Funtion 构造函数,最后一个参数会被当做函数体
- 如果只有一个参数,该参数就是函数体
- 这种写法不直观,几乎无人使用
4、函数的重复声明
- 后声明的含食宿会覆盖前面声明的函数
- 如果两个同名函数,分别是使用函数表达式声明的、和使用
function
命令声明的,那么使用函数表达式声明的会覆盖使用function
命令声明的函数,不区分先后
5、第一等公民
- 因为函数与其他数据类型地位平等,所以在 js 中被称为第一等公民
- 函数只是一个可以执行的值,对比其他数据类型,并无特殊之处
6、函数名的提升
- 使用 function 声明函数时,整个函数就会想变量一样,被提升到代码的头部
- 但是使用函数表达式声明的函数,是不会被提升,会报错的
函数的属性和方法
1、name属性:返回函数的名字
- function 声明的函数,直接返回 function 后面的函数名;
- 函数表达式声明的函数,如果 = 右边没有函数名,则返回 = 左边的变量名。如果 = 右边有函数名,则返回 = 右边的函数名
- 主要作用是当函数作为参数时,知道传入的是什么函数
2、length属性:返回函数预期传入的参数个数,参数列表中的参数个数
- 只返回定义时的参数个数,不管在调用时传入多少个参数,这个值始终不变
- 作用是可以判断定义时和调用时参数的差异,来实现面向对象编程的
方法重载(overload)
3、toString()方法:以字符串的形式返回函数的源码
- 只返回自定义函数的源码
- 对于系统函数,返回固定格式:
function (){[native code]}
- 函数内的注释也会返回,可以根据这个特性实现多行字符串
函数作用域
ES5中作用域分两种:全局作用域和函数作用域
全局作用域:整个程序中一直存在,所有地方都可读取
函数作用域:变量只在函数内部存在
1、函数内部的变量提升
看到这里,变量提升这个概念已经出现过非常多次了,在讲ES6中let的篇幅中,就着重讲过变量提升,前面的基础语法(一)中也讲到了变量提升,这个其实是允许我们在声明函数、变量前使用它们。原因就是他们的声明其本质是被提前了的。
- 在函数内部,var 声明的变量,不管在什么位置,变量声明都会被提升到函数的头部
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
2、函数本身的作用域
这里其实只要考虑两点就可以理解得非常清楚了
- 闭包值捕获
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
因为x函数在声明的时候,a=1,这个值是捕获的全局变量a的值。而在函数f中,这个a=2,表示的是局部变量a的值,所以x输出的值还是捕获的全局变量a的值
- 函数的嵌套
嵌套的调用:y中的a只在y中才声明,在x作用域中是没有这个变量的,所以找不到
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
嵌套的声明:bar作为foo作用域内部的作用域,所以能访问到x
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
函数的参数
- 函数参数是不必要的,js允许省略参数;
- 调用函数参数时传的参数个数,可以跟函数声明的参数个数完全不同;
- 调用函数参数时,按从左到右的顺序入参,不能直接省略左边的参数。如果要省略,通过传入 undefined,例如:
f(undefined, 1)
- 如果参数是
原始类型
的值,则是通过值拷贝
的方式传递,无论函数内容如何修改,都不会影响到外部的值; - 如果参数是
符合类型
的值,则是通过地址传递
的方式传递,也就是在函数内部修改参数,将会影响到外面的值;(如果是直接修改参数的引用,是不会影响到外面的值。可以将函数内部的参数看做是对象的另一个变量
); - 如果函数参数列表中出现同名的参数,则取最右边的参数;
arguments 对象
-
arguments
对象可以读取函数在运行时的所有参数,arguments[0]
、arguments[1]
、arguments[2]
…… -
arguments.length
可以读取函数在运行时的参数个数; -
arguments
可以在运行时动态修改传入的参数,例如:arguments[0]=99
,如果第一个参数是0,最后也会使用99; - 使用
'use strict'
可以禁止arguments[0]
对参数的修改,在函数头部声明下'use strict'
就可以了; -
arguments
本质是一个对象,不能使用数组的 slice、forEach 等方法; -
arguments.callee
返回它所对应的原函数,可以达到调用自己的作用,但是在严格模式('use strict'
)下是禁止的,所以不建议使用;
闭包
1、闭包的定义:定义在一个函数内部的函数
2、 闭包的特性:能记住(捕获)它被定义的环境,也就是闭包可以记住它所在的那个函数的环境
3、闭包的作用:
- 可以读取函数内部的变量;
- 让这些变量一直保存在内存中,即闭包可以使它诞生的环境一直存在;
- 封装对象的私有属性和私有方法;
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面例子中:
函数定义时的持有关系是
createIncrementor
--> function
----> start
执行代码:var inc = createIncrementor(5);
之后变成了
createIncrementor
--> function
----> start
inc
--> function
----> start
inc 将 function 函数持有,导致 function 函数无法释放。因为 createIncrementor 持有的 function 没有被释放,所以 createIncrementor 也没有被释放,从而也无法正常被垃圾回收机制回收。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
【注意】,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
立即调用的函数表达式(IIFE)
1、立即调用的函数表达式的作用
- 不必为函数命名,避免污染全局变量;
- IIFE内部形成一个单独的作用域,可以封装一些外部无法读取的私有变量;
2、IIFE的写法
- 函数实现后面接
()
; - 结尾要写封号
;
; - 避免function出现在首行,需要使用圆括号表达式,将其理解为一个表达式,而不是一个关键字;
不要返回值的格式
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
使用返回值的格式
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
eval 命令
eval 命令接收一个字符串作为参数,并将这个字符串当做语句执行
- 不要使用 eval 命令;
- eval 命令存在很严重的安全问题,可能会修改外部命令;
- 引擎只能识别 eval 的直接调用,如果是通过 eval 别名调用的,引擎一律无法识别;
- eval 的作用域就是当前它所在的作用域;
- eval 中的语句如果无法执行,则会报错;
- eval 中的语句需要有实际意义,否则有可能报错。例如:
eval('return ;
)`;
数组
- 数组的本质是对象(object);
- 数组内可以存放任何类型的数据;
- 数组的
length
属性是可读可写,可以通过将 length 设为0来清空数组。如果设置其他类型、或者是边界值之外的值,会报错; -
in
运算符:判断某个键名是否存在。适用于对象、也适用于数组; - 数组的遍历,建议通过
for
、while
、forEach
来遍历,不是使用对象的遍历for...in
,是因为它会输出数组作为对象的一些属性,超出长度之外的内容也会被输出; -
delete
运算符删除一个数组成员后,会形成空位,并不会影响数组的 length 属性; - 空位的几种样式,例如:
var a = [1, , 1];
、var a = [, , ,];
,末尾的逗号不会形成空位; - 空位在被遍历时会被跳过,而 undefined 在遍历时不会被跳过;
- 很多类似数组的对象,有 length 属性,可以通过下标取值,例如:arguments对象
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function
网友评论