JavaScript中的难点及易错点(二)

1. 难点及易错点之立即执行函数

说到立即执行函数大家可能一点都不陌生,但是当真正的深入我们会发现处处采坑,那我们接下来就从简单的开始捋一捋。
一般来说,立即执行函数表现形式有两种常见的形式,分别为:

1
2
3
4
5
// 常见形式一(括号在外面):
(function() {})();

// 常见形式二(括号在里面):
(function() {}());

接下来我们再来看看一些其他不太常见的写法:

1
2
3
4
5
6
7
8
9
void function() {} ();

!function() {}();

+function() {}();

-function() {}();

~function() {}();

有时候遇到的时候大家需要注意一下,不要被这种唬人的形式给吓到了,下面我们需要注意一下立即函数错误的一种形式:
function (){}();
在解释为什么这样写是错误时,我再给大家说一下什么是函数表达式和函数声明:

1
2
3
4
5
// 函数表达式
let f = function(){};

// 函数声明
function f(){}

当我们使用function(){}()的形式时,解析器会认为function(){}是函数声明,由于函数声明式语句后面不能有括号,因此解析器会抛出一个语法错误。
但是当我们在函数声明语句之间添加一个括号,即”(function (){})”,此时解析器会认为这是函数表达式,因此在表达式后面添加一个括号,解析器不会抛出错误。

总结一下:在函数体后面加括号就能立即调用,则这个函数必须是函数表达式,不能是函数声明。

关于常见的两种形式写法,我来解释一下为什么这么写是正确的:

1
2
3
4
5
// 常见形式一(括号在外面):
(function() {})();

// 常见形式二(括号在里面):
(function() {}());

因为Javascript引擎看到function关键字之后,认为后面跟的是函数定义语句,不应该以圆括号结尾。其解决办法就是:让引擎知道圆括号前面的部分不是函数定义语句,而是一个表达式,这两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义。

对于不太常见的形式,我们也是遵循这样的一个思路:通过符号的使用,使得解析器认为是函数表达式语句,从而实现立即函数。

2. 类型比较

下面我们先来看看两个例子:
例子1:

1
2
3
var arr = [];
var arr1 = [1];
console.log(arr === arr1); // false

例子2:

1
2
3
var arr = [];
var arr1 = [];
console.log(arr === arr1); // false

看过上面两个例子之后,可能大家还有点疑惑,没关系,大家只需要记住一点就是:两个单独的数字永远不相等

3. 对象拷贝与赋值

下面我们来看一个例子:

1
2
3
4
5
6
7
8
var obj = {
name: 'daipi173',
age: 22
};
var newObj = obj;
newObj.name = 'daipi';
console.log(obj.name); // 'daipi'
console.log(newObj.name); // 'daipi'

造成修改newObj中的name属性值时,导致obj中的name属性值也进行了一个修改的原因是:newObj对象获得的只是一个内存地址,即newObj和obj同时指向的是一个地址,导致其不是真正的一个拷贝过程。

当使用es6中的assign()方法时:

1
2
3
4
5
6
7
8
9
var obj2 = {
name: 'daipi173',
age: 22
};
var newObj2 = Object.assign({}, obj2, {color: 'blue'});
newObj2.name = 'daipi';
console.log(obj2.name); // 'daipi173'
console.log(newObj2.name); // 'daipi'
console.log(newObj2.color); // 'blue'

但是我们需要注意的一点是assign()方法是浅拷贝,并不是深拷贝。换句话来说就是:如果源对象某个属性的值是对象,那么目标对象拷贝的是这个对象的引用,我们来举一个例子:

1
2
3
4
var obj1 = {a: {b: 1} };
var obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 2

如果对象的属性值不是一个对象的话,那么就不会影响该值,举一个例子:

1
2
3
4
5
var obj1 = {a: 1};
var obj2 = Object.assign({}, obj1);
obj1.a = 2;
console.log(obj1); // {a: 2}
console.log(obj2.a); // 1

assign()方法还有许多的用处,但是在这里我只列举它的一些用处,对此很感兴趣的同学可以去百度搜索更多关于它的一些内容和方法。

4. 函数的参数

下面我们来看一个例子1:

1
2
3
4
5
function fn() {
console.log(Array.prototype.slice.call(arguments)); // [1,2,3,4]
}

fn(1,2,3,4);

Array.prototype.slice.call()能把类数组对象转化成数组,在这里如果不懂Array.prototype.slice.call()的机制的同学可以去网上百度一下,在这里我就不进行一个细说了。上面的代码利用函数中的arguments类数组对象获取传入函数的参数数组,所以输出数组[1,2,3,4]。
例子2:

1
2
3
4
5
6
7
8
function fn () {
return function () {
console.log(Array.prototype.slice.call(arguments));
}
}

fn(1,2,3,4); //此时控制台并无输出,因为它返回了一个函数体,我们需要对它执行一下
fn(1,2,3,4)(1,2,3,4); // [1,2,3,4]

例子3:

1
2
3
4
5
6
7
var args = [1,2,3,4];
function fn() {
console.log(Array.prototype.slice.call(arguments)); // [1,2,3,4,5,6,7,8]
}
Array.prototype.push.call(args,5,6,7,8);
console.log(args); // [1,2,3,4,5,6,7,8]
fn(...args);

上面的代码利用了Array.prototype.push.call()方法向args数组中插入了5、6、7、8,并利用ES6中的延展操作符(…)将数组展开并传入到fn函数中,所以控制台打印出[1,2,3,4,5,6,7,8]。