10个JavaScript难点
发布时间:2017-11-07, 15:41:37 分类:HTML | 编辑 off 网址 | 辅助
正文 7766字数 368,415阅读
1. 立即执行函数
立即执行函数,即Immediately Invoked Function Expression (IIFE),正如它的名字,就是创建函数的同时立即执行。它没有绑定任何事件,也无需等待任何异步操作:
function(){…}是一个匿名函数,包围它的一对括号将其转换为一个表达式,紧跟其后的一对括号调用了这个函数。立即执行函数也可以理解为立即调用一个匿名函数。立即执行函数最常见的应用场景就是:将var变量的作用域限制于你们函数内,这样可以避免命名冲突。
立即执行函数,即Immediately Invoked Function Expression (IIFE),正如它的名字,就是创建函数的同时立即执行。它没有绑定任何事件,也无需等待任何异步操作:
(function() {
// 代码
// ...
})();
Run code
Cut to clipboard
function(){…}是一个匿名函数,包围它的一对括号将其转换为一个表达式,紧跟其后的一对括号调用了这个函数。立即执行函数也可以理解为立即调用一个匿名函数。立即执行函数最常见的应用场景就是:将var变量的作用域限制于你们函数内,这样可以避免命名冲突。
2. 闭包
对于闭包(closure),当外部函数返回之后,内部函数依然可以访问外部函数的变量。
代码中,外部函数f1只执行了一次,变量N设为0,并将内部函数f2赋值给了变量result。由于外部函数f1已经执行完毕,其内部变量N应该在内存中被清除,然而事实并不是这样:我们每次调用result的时候,发现变量N一直在内存中,并且在累加。为什么呢?这就是闭包的神奇之处了!
对于闭包(closure),当外部函数返回之后,内部函数依然可以访问外部函数的变量。
function f1()
{
var N = 0; // N是f1函数的局部变量
function f2() // f2是f1函数的内部函数,是闭包
{
N += 1; // 内部函数f2中使用了外部函数f1中的变量N
console.log(N);
}
return f2;
}
var result = f1();
result(); // 输出1
result(); // 输出2
result(); // 输出3
Run code
Cut to clipboard
代码中,外部函数f1只执行了一次,变量N设为0,并将内部函数f2赋值给了变量result。由于外部函数f1已经执行完毕,其内部变量N应该在内存中被清除,然而事实并不是这样:我们每次调用result的时候,发现变量N一直在内存中,并且在累加。为什么呢?这就是闭包的神奇之处了!
3. 使用闭包定义私有变量
通常,JavaScript开发者使用下划线作为私有变量的前缀。但是实际上,这些变量依然可以被访问和修改,并非真正的私有变量。这时,使用闭包可以定义真正的私有变量:
代码中,对象p的的name属性为私有属性,使用p.name不能直接访问。
通常,JavaScript开发者使用下划线作为私有变量的前缀。但是实际上,这些变量依然可以被访问和修改,并非真正的私有变量。这时,使用闭包可以定义真正的私有变量:
function Product() {
var name;
this.setName = function(value) {
name = value;
};
this.getName = function() {
return name;
};
}
var p = new Product();
p.setName("Iswtf");
console.log(p.name); // 输出undefined
console.log(p.getName()); // 输出Iswtf
Run code
Cut to clipboard
代码中,对象p的的name属性为私有属性,使用p.name不能直接访问。
4. prototype
每个JavaScript构造函数都有一个prototype属性,用于设置所有实例对象需要共享的属性和方法。prototype属性不能列举。JavaScript仅支持通过prototype属性进行继承属性和方法。
代码中,x和y都是构造函数Rectangle创建的对象实例,它们通过prototype继承了getDimensions方法。
每个JavaScript构造函数都有一个prototype属性,用于设置所有实例对象需要共享的属性和方法。prototype属性不能列举。JavaScript仅支持通过prototype属性进行继承属性和方法。
function Rectangle(x, y)
{
this._length = x;
this._breadth = y;
}
Rectangle.prototype.getDimensions = function()
{
return {
length: this._length,
breadth: this._breadth
};
};
var x = new Rectangle(3, 4);
var y = new Rectangle(4, 3);
console.log(x.getDimensions()); // { length: 3, breadth: 4 }
console.log(y.getDimensions()); // { length: 4, breadth: 3 }
Run code
Cut to clipboard
代码中,x和y都是构造函数Rectangle创建的对象实例,它们通过prototype继承了getDimensions方法。
5. 模块化
JavaScript并非模块化编程语言,至少ES6落地之前都不是。然而对于一个复杂的Web应用,模块化编程是一个最基本的要求。这时,可以使用立即执行函数来实现模块化,
所谓模块化,就是根据需要控制模块内属性与方法的可访问性,即私有或者公开。在代码中,module为一个独立的模块,N为其私有属性,print为其私有方法,decription为其公有属性,add为其共有方法。
JavaScript并非模块化编程语言,至少ES6落地之前都不是。然而对于一个复杂的Web应用,模块化编程是一个最基本的要求。这时,可以使用立即执行函数来实现模块化,
var module = (function() {
var N = 5;
function print(x) {
console.log("The result is: " + x);
}
function add(a) {
var x = a + N;
print(x);
}
return {
description: "This is description",
add: add
};
})();
console.log(module.description); // 输出"this is description"
module.add(5); // 输出“The result is: 10”
Run code
Cut to clipboard
所谓模块化,就是根据需要控制模块内属性与方法的可访问性,即私有或者公开。在代码中,module为一个独立的模块,N为其私有属性,print为其私有方法,decription为其公有属性,add为其共有方法。
6. 变量提升
JavaScript会将所有变量和函数声明移动到它的作用域的最前面,这就是所谓的变量提升(Hoisting)。也就是说,无论你在什么地方声明变量和函数,解释器都会将它们移动到作用域的最前面。因此我们可以先使用变量和函数,而后声明它们。
但是,仅仅是变量声明被提升了,而变量赋值不会被提升。如果你不明白这一点,有时则会出错:
上面的代码等价于下面的代码:
为了避免BUG,开发者应该在每个作用域开始时声明变量和函数。
JavaScript会将所有变量和函数声明移动到它的作用域的最前面,这就是所谓的变量提升(Hoisting)。也就是说,无论你在什么地方声明变量和函数,解释器都会将它们移动到作用域的最前面。因此我们可以先使用变量和函数,而后声明它们。
但是,仅仅是变量声明被提升了,而变量赋值不会被提升。如果你不明白这一点,有时则会出错:
console.log(y); // 输出undefined
y = 2; // 初始化y
Run code
Cut to clipboard
上面的代码等价于下面的代码:
var y; // 声明y
console.log(y); // 输出undefined
y = 2; // 初始化y
Run code
Cut to clipboard
为了避免BUG,开发者应该在每个作用域开始时声明变量和函数。
7. 柯里化
柯里化,即Currying,可以是函数变得更加灵活。我们可以一次性传入多个参数调用它;也可以只传入一部分参数来调用它,让它返回一个函数去处理剩下的参数。
代码中,我们可以一次性传入2个1作为参数add(1)(1),也可以传入1个参数之后获取add1与add10函数,这样使用起来非常灵活。
柯里化,即Currying,可以是函数变得更加灵活。我们可以一次性传入多个参数调用它;也可以只传入一部分参数来调用它,让它返回一个函数去处理剩下的参数。
var add = function(x) {
return function(y) {
return x + y;
};
};
console.log(add(1)(1)); // 输出2
var add1 = add(1);
console.log(add1(1)); // 输出2
var add10 = add(10);
console.log(add10(1)); // 输出11
Run code
Cut to clipboard
代码中,我们可以一次性传入2个1作为参数add(1)(1),也可以传入1个参数之后获取add1与add10函数,这样使用起来非常灵活。
8. apply, call与bind方法
JavaScript开发者有必要理解apply、call与bind方法的不同点。它们的共同点是第一个参数都是this,即函数运行时依赖的上下文。
三者之中,call方法是最简单的,它等价于指定this值调用函数:
apply方法与call方法类似。两者唯一的不同点在于,apply方法使用数组指定参数,而call方法每个参数单独需要指定:
apply(thisArg, [argsArray])
call(thisArg, arg1, arg2, …)
使用bind方法,可以为函数绑定this值,然后作为一个新的函数返回:
JavaScript开发者有必要理解apply、call与bind方法的不同点。它们的共同点是第一个参数都是this,即函数运行时依赖的上下文。
三者之中,call方法是最简单的,它等价于指定this值调用函数:
var user = {
name: "Rahul Mhatre",
whatIsYourName: function() {
console.log(this.name);
}
};
user.whatIsYourName(); // 输出"Rahul Mhatre",
var user2 = {
name: "Neha Sampat"
};
user.whatIsYourName.call(user2); // 输出"Neha Sampat"
Run code
Cut to clipboard
apply方法与call方法类似。两者唯一的不同点在于,apply方法使用数组指定参数,而call方法每个参数单独需要指定:
apply(thisArg, [argsArray])
call(thisArg, arg1, arg2, …)
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};
var greet1 = {
greet: "Hola"
};
user.greetUser.call(greet1, "Rahul"); // 输出"Hola Rahul"
user.greetUser.apply(greet1, ["Rahul"]); // 输出"Hola Rahul"
Run code
Cut to clipboard
使用bind方法,可以为函数绑定this值,然后作为一个新的函数返回:
var user = {
greet: "Hello!",
greetUser: function(userName) {
console.log(this.greet + " " + userName);
}
};
var greetHola = user.greetUser.bind({greet: "Hola"});
var greetBonjour = user.greetUser.bind({greet: "Bonjour"});
greetHola("Rahul") // 输出"Hola Rahul"
greetBonjour("Rahul") // 输出"Bonjour Rahul"
Run code
Cut to clipboard
9. Memoization
Memoization用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。
代码中,第2次计算fibonacci(100)则只需要在内存中直接读取结果。
Memoization用于优化比较耗时的计算,通过将计算结果缓存到内存中,这样对于同样的输入值,下次只需要中内存中读取结果。
function memoizeFunction(func)
{
var cache = {};
return function()
{
var key = arguments[0];
if (cache[key])
{
return cache[key];
}
else
{
var val = func.apply(this, arguments);
cache[key] = val;
return val;
}
};
}
var fibonacci = memoizeFunction(function(n)
{
return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(100)); // 输出354224848179262000000
console.log(fibonacci(100)); // 输出354224848179262000000
Run code
Cut to clipboard
代码中,第2次计算fibonacci(100)则只需要在内存中直接读取结果。
10. 函数重载
所谓函数重载(method overloading),就是函数名称一样,但是输入输出不一样。或者说,允许某个函数有各种不同输入,根据不同的输入,返回不同的结果。凭直觉,函数重载可以通过if…else或者switch实现,这就不去管它了。jQuery之父John Resig提出了一个非常巧(bian)妙(tai)的方法,利用了闭包。
从效果上来说,people对象的find方法允许3种不同的输入: 0个参数时,返回所有人名;1个参数时,根据firstName查找人名并返回;2个参数时,根据完整的名称查找人名并返回。
难点在于,people.find只能绑定一个函数,那它为何可以处理3种不同的输入呢?它不可能同时绑定3个函数find0,find1与find2啊!这里的关键在于old属性。
由addMethod函数的调用顺序可知,people.find最终绑定的是find2函数。然而,在绑定find2时,old为find1;同理,绑定find1时,old为find0。3个函数find0,find1与find2就这样通过闭包链接起来了。
根据addMethod的逻辑,当f.length与arguments.length不匹配时,就会去调用old,直到匹配为止。
参考链接
闭包 - MDN
JavaScript闭包-块级作用域和私有变量
Javascript继承机制的设计思想 - 阮一峰
变量提升 - MDN
JS函数式编程指南
浅谈JavaScript函数重载
所谓函数重载(method overloading),就是函数名称一样,但是输入输出不一样。或者说,允许某个函数有各种不同输入,根据不同的输入,返回不同的结果。凭直觉,函数重载可以通过if…else或者switch实现,这就不去管它了。jQuery之父John Resig提出了一个非常巧(bian)妙(tai)的方法,利用了闭包。
从效果上来说,people对象的find方法允许3种不同的输入: 0个参数时,返回所有人名;1个参数时,根据firstName查找人名并返回;2个参数时,根据完整的名称查找人名并返回。
难点在于,people.find只能绑定一个函数,那它为何可以处理3种不同的输入呢?它不可能同时绑定3个函数find0,find1与find2啊!这里的关键在于old属性。
由addMethod函数的调用顺序可知,people.find最终绑定的是find2函数。然而,在绑定find2时,old为find1;同理,绑定find1时,old为find0。3个函数find0,find1与find2就这样通过闭包链接起来了。
根据addMethod的逻辑,当f.length与arguments.length不匹配时,就会去调用old,直到匹配为止。
function addMethod(object, name, f)
{
var old = object[name];
object[name] = function()
{
// f.length为函数定义时的参数个数
// arguments.length为函数调用时的参数个数
if (f.length === arguments.length)
{
return f.apply(this, arguments);
}
else if (typeof old === "function")
{
return old.apply(this, arguments);
}
};
}
// 不传参数时,返回所有name
function find0()
{
return this.names;
}
// 传一个参数时,返回firstName匹配的name
function find1(firstName)
{
var result = [];
for (var i = 0; i < this.names.length; i++)
{
if (this.names[i].indexOf(firstName) === 0)
{
result.push(this.names[i]);
}
}
return result;
}
// 传两个参数时,返回firstName和lastName都匹配的name
function find2(firstName, lastName)
{
var result = [];
for (var i = 0; i < this.names.length; i++)
{
if (this.names[i] === (firstName + " " + lastName))
{
result.push(this.names[i]);
}
}
return result;
}
var people = {
names: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};
addMethod(people, "find", find0);
addMethod(people, "find", find1);
addMethod(people, "find", find2);
console.log(people.find()); // 输出["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); // 输出["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean", "Edwards")); // 输出["Dean Edwards"]
Run code
Cut to clipboard
参考链接
闭包 - MDN
JavaScript闭包-块级作用域和私有变量
Javascript继承机制的设计思想 - 阮一峰
变量提升 - MDN
JS函数式编程指南
浅谈JavaScript函数重载
(支付宝)给作者钱财以资鼓励 (微信)→
有过 3 条评论 »
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
箭头函数与传统的JavaScript函数主要区别在于以下几点:
1、对 this 的关联。函数内置 this 的值,取决于箭头函数在哪儿定义,而非箭头函数执行的上下文环境。 2 、new 不可用。箭头函数不能使用 new 关键字来实例化对象,不然会报错。 3、this 不可变。函数内置 this 不可变,在函数体内整个执行环境中为常量。 4、没有arguments对象。更不能通过arguments对象访问传入参数。只能使用显式命名或其他ES6新特性来完成。
这些差异的存在是有理可循的。首先,对this的绑定是JavaScript错误的常见来源之一。容易丢失函数内置数值,或得出意外结果。其次,将箭头函数限制为使用固定this引用,有利于JavaScript引擎优化处理。
语法:
箭头函数的语法很简单,定义自变量,然后是箭头和函数主体。
1、不引入参数:
var sum = () => 1 + 2; // 等同于: var sum = function() { return 1 + 2; };
2、引入单个参数:
var reflect = value => value; // 等同于: var reflect = function(value) { return value; }; x => x * x //等同于: function (x) { return x * x; }
传一个参数就直接写就好了,不用加小括号。
句,所以也不用加大括号。
函数构造完毕赋给reflect加以引用。
3、引入多个参数,则应加上小括号
var sum = (num1, num2) => num1 + num2; // 等同于: var sum = function(num1, num2) { return num1 + num2; };
4、若你想使用标准的函数体,或者函数体内可能有更多的语句要执行,则要用大括号将函数体括起来,并明确定义返回值。
var sum = (num1, num2) => { return num1 + num2; } //等同于: var sum = function(num1, num2) { return num1 + num2; };
大括号内的部分基本等同于传统函数
5、箭头函数若要返回自定义对象的话,就必须用小括号把该对象括起来先
var getTempItem = id = > ({ id: id, name: "Temp" }); // 等同于: var getTempItem = function(id) { return { id: id, name: "Temp" }; };
用小括号包含大括号则是对象的定义,而非函数主体
箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象 return fn(); } }; obj.getAge(); // 25
如果使用箭头函数,以前的那种hack写法:
var that = this;
就不再需要了。
由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = { birth: 1990, getAge: function (year) { var b = this.birth; // 1990 var fn = (y) => y - this.birth; // this.birth仍是1990 return fn.call({birth:2000}, year); } }; obj.getAge(2015); // 25
function hi(){ var a = 1; return function(){ console.log(a++); }; }; var aaa = hi(); var bbb = hi; aaa(); aaa(); bbb();
这段代码中 aaa与bbb的区别是什么?
aaa 是将 hi() 的运行结果赋值给它,即 return 返回的匿名函数,此时有一个闭包,则每次调用 aaa 时都访问的同一个 a,aaa() 第一次运行结果为 1,第二次为2 而 bbb 将是将 hi 这个函数名赋值给它,则调用 bbb() 后返回一个函数表达式,即function(){console.log(a++)};
bbb就是hi本身,换句话说就是外层的function。bbb()的结果应该跟hi()的结果一样。 aaa是hi()调用之后返回的那个东西,也就是说它是里层的function,a是它闭包里面的一个对象。aaa()的调用结果是console.log(a++)。
hi()是将hi函数运行之后返回 function(){console.log(a++)},这种返回值是函数的方法叫做函数式编程,楼主可以查找下相关的资料看看。 aaa 的值是 function(){console.log(a++)},而aaa再次运行的话就会打印出来a的值,这个地方可以看下上面那个hi函数,其中包含一个闭包,也就是说hi函数返回的function(){console.log(a++)}这个函数会一直保持着对引用,也就是说每调用一次 aaa ,那么打印的值都会加上1,关于具体的闭包,楼主还是可以自己查查资料 bbb 的值就是hi函数,这是一个普通的赋值操作
for (var i = 0; i < 5; i++) { console.log(i); //直接输出 0 到 4 } for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000 * i); //输出一个 5,然后每隔一秒再输出一个 5,一共 5 个 5 } for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); //输出 0 到 4 })(i); } for (var i = 0; i < 5; i++) { (function() { setTimeout(function() { console.log(i); }, i * 1000); })(i); //输出 5 } for (var i = 0; i < 5; i++) { setTimeout((function(i) { console.log(i); })(i), i * 1000); //立马输出 0 到 4 } /* 这里给 setTimeout 传递了一个立即执行函数。setTimeout 可以接受函数或者字符串作为参数,那么这里立即执行函数是个 undefined ,也就是说等价于: setTimeout(undefined, ...); 而立即执行函数会立即执行,那么应该是立马输出的。 */ setTimeout(function() { console.log(1) }, 0); new Promise(function executor(resolve) { console.log(2); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(3); }).then(function() { console.log(4); }); console.log(5); /* 首先先碰到一个 setTimeout,于是会先设置一个定时,在定时结束后将传递这个函数放到任务队列里面,因此开始肯定不会输出 1 。 然后是一个 Promise,里面的函数是直接执行的,因此应该直接输出 2 3 。 然后,Promise 的 then 应当会放到当前 tick 的最后,但是还是在当前 tick 中。 因此,应当先输出 5,然后再输出 4 。 最后在到下一个 tick,就是 1 。 “2 3 5 4 1” */