本章是第三章函数相关的内容。函数包括了作用域、原型链、闭包等核心知识点,非常关键。
在学完后,希望掌握下面知识点:
在JavaScript中,函数实际也是一种对象,每个函数都是Function类型的实例,能够定义不同类型的属性与方法。
函数的定义大致可以分为 3 种:
function关键字+函数名+形参+函数体
function sum(num1, num2) {
return num1 + num2;
}
函数表达式的形式类似于普通变量的初始化,只不过这个变量初始化的值是一个函数:
var sum = function (num1, num2) {
return num1 + num2;
};
这个函数表达式没有名称,属于匿名函数表达式
但是也可以像上面函数声明一样定义一个函数名,但是这样它只是函数内部的一个局部变量,在函数外部无法直接调用:
var sum = function foo(num1, num2) {
return num1 + num2;
};
console.log(foo(1,3)); //ReferenceError: foo is not defined
如果想正常还是需要使用前面的sum
使用new
操作符,调用Function()构造函数,传入对应的参数:
var add = new Function("a","b","return a + b");
只有最后一个参数是执行的函数体,其他都是函数的形参。
一般用的少,主要有下面两个缺点:
对于第二点,可以参考下面代码:
var y = 'global'; // 全局环境定义的y值
function constructFunction() {
var y = 'local'; // 局部环境定义的y值
return new Function('return y'); // 无法获取局部环境定义的值
}
console.log(constructFunction()()); // 输出'global'
JavaScript解释器在处理两者时有一定区别。
函数的调用大致分为 5 种模式:
通过函数声明或者函数表达式的方式定义函数,然后直接通过函数名调用的模式
// 函数声明
function add(a1, a2) {
return a1 + a2;
}
// 函数表达式
var sub = function (a1, a2) {
return a1 - a2;
};
add(1, 3);
sub(4, 1);
优先定义一个对象obj,然后在对象内部定义值为函数的属性property,通过对象obj.property()来进行函数的调用
//定义对象
var obj = {
name:"kingx",
//定义getName属性,值为一个函数
getName:function(){
return this.name;
}
};
obj.getName(); //通过对象进行调用
函数还可以通过中括号来调用,即 对象名['函数名']
,那么上面的实例代码,我们还可以改写成如下代码
obj["getName"]();
如果在某个方法中返回的是函数对象本身this,那么可以利用链式调用原理进行连续的函数调用
var obj2 = {
name: 'kingx',
getName: function () {
console.log(this.name);
},
setName: function (name) {
this.name = name;
return this; // 在函数内部返回函数对象本身
}
};
obj2.setName('kingx2').getName(); // 链式函数调用
定义一个函数,在函数中定义实例属性,在原型上定义函数,然后通过new操作符生成函数的实例,再通过实例调用原型上定义的函数。
// 定义函数对象
function Person(name) {
this.name = name
}
// 原型上定义函数
Person.prototype.getName = function(){
return this.name;
};
//通过new操作符生成实例
var p = new Person("kingx");
//通过实例进行函数的调用
p.getName();
通过call()
函数或者apply()
函数可以改变函数执行的主体,使得某些不具有特定函数的对象可以直接调用该特定函数。
// 定义一个函数
function sum(num1, num2) {
return num1 + num2;
}
// 定义一个对象
var person = {};
// 通过call()函数与apply()函数调用sum()函数
sum.call(person, 1, 2);
sum.apply(person, [1, 2]);
通过call()
函数与apply()
函数,使得没有sum()函数的person对象也可以直接调用 sum()函数。这两个函数具体相关的内容会在本章后面的部分涉及到。
匿名函数,顾名思义就是没有函数名称的函数。
匿名函数的调用有 2 种方式:
()
将匿名函数括起来,然后在后面使用小括号()
,传递对应的参数,进行调用// 方式一
// 通过函数表达式定义匿名函数,并赋给变量sum
var sum = function(num1, num2){
return num1 + num2;
};
// 通过sum()函数进行匿名函数调用
sum(1, 2);
// 方式二
(function (num1, num2) {
return num1 + num2;
})(1, 2); // 3
上述方式中,使用小括号括住的函数声明实际上是一个函数表达式,紧随其后的小括号表示会立即调用这个函数
函数必须要用小括号括起来,不然会出问题。
自执行函数即函数定义和函数调用的行为先后连续产生。它需要以一个函数表达式的身份进行函数调用,上面的匿名函数调用也属于自执行函数的一种。
下面是自执行函数的多种表现形式:
function (x) {
alert(x);
}(5); // 抛出异常,Uncaught SyntaxError: Unexpected token
var aa = function(x) {
console.log(x);
}(1); // 1
true && function(x){
console.log(x);
}(2); // 2
0, function(x) {
console.log(x);
}(3); // 3
!function(x) {
console.log(x);
}(4); // 4
~function(x) {
console.log(5);
}(5); // 5
-function(x) {
console.log(x);
}(6); // 6
+function(x) {
console.log(x);
}(7); // 7
new function(x) {
console.log(x);
}(8); // 8
new function() {
console.log(9); // 9
}
形参和实参区别:
JavaScript是弱类型语言,函数参数在上述规则外还有一些特性:
arguments对象是所有函数都具有的一个内置局部变量,表示的是函数实际接收的参数,是一个类数组结构(除了具有length属性外,不 具有数组的一些常用方法)
arguments对象是一个类数组结构,可以通过索引访问,每一项表示对应传递的实参值,如果该项索引值不存在,则会返回undefined
function sum(num1, num2) {
console.log(arguments[0]); // 3
console.log(arguments[1]); // 4
console.log(arguments[2]); // undefined
}
sum(3, 4);
arguments对象的值由实参决定,而不是由定义的形参决定,形参与arguments对象占用独立的内存空间。
arguments对象与形参之间的关系:
length
属性在函数调用的时候就已经确定,不会随着函数的处理而改变undefined
function foo(a, b, c) {
console.log(arguments.length); // 2
arguments[0] = 11;
console.log(a); // 11
b = 12;
console.log(arguments[1]); // 12
arguments[2] = 3;
console.log(c); // undefined
c = 13;
console.log(arguments[2]); // undefined
console.log(arguments.length); // 2
}
foo(1, 2);
arguments对象有一个很特殊的属性callee,表示的是当前正在执行的函数,在比较时是严格相等的。
在匿名函数的递归中非常有用,因为匿名函数没有名称,所以只能通过arguments.callee属性来获取函数自身,同时传递参数进行函数调用
function create() {
return function (n) {
if (n <= 1)
return 1;
return n * arguments.callee(n - 1);
}
}
var result = creat()(5); // 120 (5*4*3*2*1)
但并不推荐广泛使用,因为这个属性会改变函数内部的 this 值。
所以如果需要在函数内部进行递归调用,推荐使用函数声明或者使用函数表达式,给函数一个明确的函数名
比如定义函数时,明确要求在调用时只能传递正好3个参数,否则都抛出异常
function f(x, y, z) {
// 检查传递的参数个数是否正确
if (arguments.length !== 3) {
throw new Error("期望传递的参数个数为3,实际传递个数为" + arguments.length);
}
// ...do something
}
f(1, 2); // Uncaught Error: 期望传递的参数个数为3,实际传递个数为2
定义一个函数,该函数只会特定处理传递的前几个参数,对于后面的参数不论传递多少个都会统一处理,这种场景下我们可以使用arguments对象。
例如,定义一个函数,需要将多个字符串使用分隔符相连,并返回一个结果字符串。 此时第一个参数表示的是分隔符,而后面的所有参数表示待相连的字符串,我们并不关心后面待连接的字符串有多少个,通过arguments对象统一处理即可。
function joinStr(seperator) {
// arguments对象是一个类数组结构,可以通过call()函数间接调用slice()函数,得到一个数组 var
strArr = Array.prototype.slice.call(arguments, 1);
// strArr数组直接调用join()函数
return strArr.join(seperator);
}
joinStr('-', 'orange', 'apple', 'banana'); // orange-apple-banana
joinStr(',', 'orange', 'apple', 'banana'); // orange,apple,banana
函数重载表示的是在函数名相同的情况下,通过函数形参的不同参数类型或者不同参数个数来定义不同的函数。
JavaScript中是没有函数重载的,原因是:
通过模拟函数重载可以完成类似任务,比如想写出一个通用函数,来实现任意个数字的加法运算求和:
// 通用求和函数
function sum() {
// 通过call()函数间接调用数组的slice()函数得到函数参数的数组
var arr = Array.prototype.slice.call(arguments);
// 调用数组的reduce()函数进行多个值的求和
return arr.reduce(function (pre, cur) {
return pre + cur;
}, 0)
}
sum(1, 2); // 3
sum(1, 2, 3); // 6
sum(1, 2, 3, 4); // 10
当我们创建对象的实例时,通常会使用到构造函数,例如对象和数组的实例化可以通过相应的构造函数Object()和Array()完成。
构造函数与普通函数在语法的定义上没有任何区别,主要的区别体现在以下3点:
// 对于第 2 条
function Person(name) {
this.name = name;
}
var p = new Person('kingx');
console.log(p); // Person {name: "kingx"}
//对于第 3 条
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
alert(this.name);
};
}
var person = new Person('kingx', '12');
person.sayName(); // 'kingx'
一个函数在当作普通函数使用时,函数内部的this会指向window
Person('kingx', '12');
window.sayName(); // 'kingx'
使用构造函数可以在任何时候创建我们想要的对象实例,构造函数在执行时会执行以下4步:
使用构造函数的问题在于,每创建一个新的实例,都会新增一个属性,例如上面的sayName(),而且不同实例中的该属性并不相同。而事实上当我们在创建对象的实例时,对于相同的函数并不需要重复创建,而且由于this的存在,总是可以在实例中访问到它具有的属性。
更好的解决办法就是通过原型,这个会在第 4 章涉及。
JavaScript中会出现变量在定义之前就可以被访问到而不会抛出异常,以及函数在定义之前就可以被调用而不会抛出异常。
这是由于JavaScript存在变量提升和函数提升机制。
作用域:一个变量的定义与调用所在的固定的范围
作用域可以分为:
变量提升:在函数作用域中,会出现的现象。将变量的声明提升到函数顶部的位置,而变量的赋值并不会被提升
会产生提升的变量必须是通过var关键字定义的,而不通过var关键字定义的全局变量是不会产生变量提升的。
比如下面的例子:
var v = 'Hello World';
(function () {
console.log(v);
var v = 'Hello JavaScript';
})(); // undefined
这是因为出现了变量提升,在函数内部,变量 v 的定义会提升到函数顶部,而赋值并不会提升,这样实际执行的代码其实是如下所示:
var v = 'Hello World';
(function () {
var v; // 变量的声明得到提升
console.log(v);
v = 'Hello JavaScript'; // 变量的赋值并未提升
})();
在window上定义了一个变量v,赋值为Hello World,而且在立即执行函数的内部同样定义了一个变量v,但是赋值语句并未提升,因此v为undefined。在输出时,会优先在函数内部作用域中寻找变量,而变量已经在内部作用域中定义,因此直接输出“undefined”。
除了通过var定义的变量会出现提升,使用函数声明方式定义的函数也会出现提升。
例如:
// 函数提升
foo(); // 我来自 foo
function foo() {
console.log("我来自 foo");
}
在上面的代码中,foo()函数的声明在调用之后,但是却可以调用成功,因为foo()函数被提升至作用域顶部。
需要注意的是函数提升会将整个函数体一起进行提升,包括里面的执行逻辑。
对于函数表达式,是不会进行函数提升的。
下面的例子展示了同时使用函数声明和函数表达式的情况:
show(); // 你好
var show;
// 函数声明,会被提升
function show() {
console.log('你好');
}
// 函数表达式,不会被提升
show = function () {
console.log('hello');
};
由于函数声明会被提升,因此最后输出的结果为“你好”
一般定义一个函数就会产生一个函数作用域,在函数体中的局部变量会在这个函数作用域中使用,一旦函数执行完成,函数所占空间就会被回收,存在于函数体中的局部变量同样会被回收,回收后将不能被访问到。但是闭包可以实现在函数执行完成后,函数中的局部变量仍然可以被访问到。
每段代码的执行都会存在于一个执行上下文环境中,每个执行上下文环境又都会存在于整体的执行上下文环境中。根据栈先进后出的特点,全局环境产生的执行上下文环境会最先压入栈中,存在于栈底。当新的函数进行调用时,会产生的新的执行上下文环境,也会压入栈中。当函数调用完成后,这个上下文环境及其中的数据都会被销毁,并弹出栈,从而进入之前的执行上下文环境中。
需要注意的是,处于活跃状态的执行上下文环境只能同时有一个。
1 var a = 10; // 1.进入全局执行上下文环境
2 var fn = function (x) {
3 var c = 10;
4 console.log(c + x);
5};
6 var bar = function(y) {
7 var b = 5;
8 fn(y + b); // 3.进入fn()函数执行上下文环境
9 };
10 bar(20); // 2.进入bar()函数执行上下文环境
像上面这种代码执行完毕,执行上下文环境就会被销毁的场景,是一种比较理想的情
况。
有另外一种情况,虽然代码执行完毕,但执行上下文环境却被无法干净地销毁,这就
是闭包。
闭包:一个拥有许多变量和绑定了这些变量执行上下文环境的表达式,通常是一个函数。
闭包有 2 个很明显的特点:
在JavaScript中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
例如
1 function fn() {
2 var max = 10;
3 return function bar(x){
4 if (x > max) {
5 console.log(x);
6 }
7 };
8 }
9 var f1 = fn();
10 f1(11); // 11
从上面可以看出闭包所存在的最大的一个问题就是消耗内存,如果闭包使用越来越多,内存消耗将越来越大。
闭包不会释放外部变量的引用,所以能将外部变量值缓存在内存中。
所以在下面的场景中应用闭包:对于一个处理很耗时的函数对象,为了避免每次都非常耗时地调用,可以将其结果缓存起来,这样如果内存中有就直接返回,没有的话再调用函数进行计算,更新缓存并返回结果。
var cachedBox = (function () {
// 缓存的容器
var cache = {};
return {
searchBox: function (id) {
// 如果在内存中,则直接返回
if(id in cache) {
return '查找的结果为:' + cache[id];
}
// 经过一段很耗时的dealFn()函数处理
var result = dealFn(id);
// 更新缓存的结果
cache[id] = result;
// 返回计算的结果
return '查找的结果为:' + result;
}
};
})();
// 处理很耗时的函数
function dealFn(id) {
console.log('这是一段很耗时的操作');
return id;
}
// 两次调用searchBox()函数
console.log(cachedBox.searchBox(1)); //这是一段很耗时的操作 查找的结果为:1
console.log(cachedBox.searchBox(1)); //查找的结果为:1
而第二次执行searchBox(1)函数时,由于第一次已经将结果更新到cache对象中,并且该对象引用并未被回收,因此会直接从内存的cache对象中读取
在JavaScript中提倡的模块化思想是希望将具有一定特征的属性封装到一起,只需要对外暴露对应的函数,并不关心内部逻辑的实现。
比如借助数组实现一个栈,只对外暴露出表示入栈和出栈的push()函数和 pop()函数,以及表示栈长度的size()函数
var stack = (function () {
// 使用数组模仿栈的实现
var arr = [];
// 栈
return {
push: function (value) {
arr.push(value);
},
pop: function () {
return arr.pop();
},
size: function () {
return arr.length;
}
};
})();
stack.push('abc');
stack.push('def');
console.log(stack.size()); // 2
stack.pop();
console.log(stack.size()); // 1
上面的代码中存在一个立即执行函数,在函数内部会产生一个执行上下文环境,最后返回一个表示栈的对象并赋给stack变量。在匿名函数执行完毕后,其执行上下文环境并不会被销毁,因为在对象的push()、pop()、size()等函数中包含了对arr变量的引用,arr 变量会继续存在于内存中,所以后面几次对stack变量的操作会使stack变量的长度产生变化。
这一部分的感受就是了解后能更加理解闭包,感觉很有必要好好看下来帮助翻过三座大山中的一座。
比较容易想到的就是下面的代码
var lis = document.getElementsByTagName('ul')[0].children;
for (var i = 0; i < lis.length; i++) {
lis[i].onclick = function () {
console.log(i);
};
}
目标是依次输出索引值0,1,2,3,4(假如有五个列表项),实际上上面代码会输出5个5。
这里涉及到了同步异步任务的问题。
原因是:执行顺序从上到下,遇到for循环,它是同步任务先执行,遇到绑定点击事件也是同步任务对其一一进行绑定。再执行遇到function函数,这是一个异步任务,当点击时将会进入队列,等待同步任务执行完毕再执行,因此同步任务执行完毕也就是for循环完成,此时的i=5,再执行function函数体内的console.log()打印都是i=5。
此处参考了这篇博客
通过闭包就能解决这样的问题:
var lis = document.getElementsByTagName('ul')[0].children;
for (var i = 0; i < lis.length; i++) {
(function (index) {
lis[index].onclick = function () {
console.log(index);
};
})(i);
}
在每一轮的for循环中,我们将索引值 i 传入一个匿名立即执行函数中,在该匿名函数中存在对外部变量lis的引用,因此会形成一个闭包。而闭包中的变量index,即外部传入的 i 值会继续存在于内存中,所以当单击 li 时,就会输出对应的索引index值。
定时器setTimeout()函数和for循环在一起使用,经常容易出问题。
var arr = ['one', 'two', 'three'];
for(var i = 0; i < arr.length; i++) {
setTimeout(function () {
console.log(arr[i]);
}, i * 1000);
}
本题是希望通过定时器从第一个元素开始往后,每隔一秒输出arr数组中的一个元素。但是运行过后,我们却会发现结果是每隔一秒输出一个“undefined”。
其实问题大致和上面一题相同,setTimeout()函数与for循环在调用时会产生两个独立执行上下文环境,当 setTimeout()函数内部的函数执行时,for循环已经执行结束,而for循环结束的条件是最后一次i++执行完毕,此时i的值为3,所以实际上setTimeout()函数每次执行时,都会输出arr[3]的值。而因为arr数组最大索引值为2,所以会间隔一秒输出“undefined”
通过闭包可以解决问题
var arr = ['one', 'two', 'three'];
for(var i = 0; i < arr.length; i++) {
(function (time) {
setTimeout(function () {
console.log(arr[time]);
}, time * 1000);
})(i);
}
通过立即执行函数将索引i作为参数传入,在立即函数执行完成后,由于 setTimeout() 函数中有对 arr 变量的引用,其执行上下文环境不会被销毁,因此对应的 i 值都会存在内存中。所以每次执行 setTimeout() 函数时,i都会是数组对应的索引值0、1、 2,从而间隔一秒输出“one”“two”“three”。
闭包往往会涉及作用域链问题,尤其是包含this属性时
var name = 'outer';
var obj = {
name: 'inner',
method: function () {
return function () {
return this.name;
}
}
};
console.log(obj.method()()); // outer
在调用obj.method()函数时,会返回一个匿名函数,而该匿名函数中返回的是 this.name,因为引用到了this属性,在匿名函数中,this相当于一个外部变量,所以会形成一个闭包。在JavaScript中,this指向的永远是函数的调用实体,而匿名函数的实体是全局对象 window,因此会输出全局变量name的值“outer”。
如果想要输出obj对象自身的name属性,就要改变this的指向,将其指向obj对象本身:
var name = 'outer';
var obj = {
name: 'inner',
method: function () {
// 用_this保存obj中的this
var _this = this;
return function () {
return _this.name;
}
}
};
console.log(obj.method()()); // inner
在method()函数中利用_this变量保存obj对象中的this,在匿名函数的返回值中再去 调用_this.name,此时_this就指向obj对象了,因此会输出“inner”
// 第一个foo()函数
function foo(a, b) {
console.log(b);
return {
// 第二个foo()函数
foo: function (c) {
// 第三个foo()函数
return foo(c, a);
}
}
}
var x = foo(0); x.foo(1); x.foo(2); x.foo(3); // undefined,0,0,0
var y = foo(0).foo(1).foo(2).foo(3); // undefined,0,1,2
var z = foo(0).foo(1); z.foo(2); z.foo(3); // undefined,0,1,1
在上面的代码中,出现了3个具有相同函数名的foo()函数,返回的第三个foo()函数中包含了对第一个foo()函数参数a的引用,因此会形成一个闭包。
这道题的关键就是理解三个foo()函数的指向。具体步骤就不说了,按照前面讲过的那些思路应该能想明白,参考最后三个x,y,z的输出来验证即可
闭包如果使用合理,在一定程度上能提高代码执行效率;如果使用不合理,则会造成内存浪费,性能下降。
当我们想要创建一个构造函数的实例时,需要使用new操作符,函数执行完成后,函数体中的this就指向了这个实例,通过下面这个实例可以访问到绑定在this上的属性。
假如我们将Person()函数当作一个普通的函数执行,其中的this则会直接指向window对象。
总的来说,可以概括为,在JavaScript中,this指向的永远是函数的调用者。
下面就是在不同场景中 this 的指向问题。
当函数没有所属对象而直接调用时,this指向的是全局对象
var value = 10;
var obj = {
value: 100,
method: function () {
var foo = function () {
console.log(this.value); // 10
console.log(this); // Window对象
};
foo();
return this.value;
}
};
obj.method();
当我们调用obj.method()函数时,foo()函数被执行,但是此时foo()函数的执行是没有所属对象的,因此this会指向全局的window对象,在输出this.value时,实际是输出window.value,因此输出“10”。
同样沿用场景1中的代码,我们修改最后一行代码,输出obj.method()函数的返回值。
console.log(obj.method()); // 100
obj.method()函数的返回值是this.value,method()函数的调用体是obj对象,此时this就指向obj对象,而obj.value = 100,因此会输出“100”。
当通过new操作符调用构造函数生成对象的实例时,this指向该实例。
// 全局变量
var number = 10;
function Person() {
// 复写全局变量
number = 20;
// 实例变量
this.number = 30;
}
// 原型函数
Person.prototype.getNumber = function () {
return this.number;
};
// 通过new操作符获取对象的实例
var p = new Person();
console.log(p.getNumber()); // 30
在上面这段代码中,我们定义了全局变量number和实例变量number,通过new操作符生成Person对象的实例p后,在调用getNumber()操作时,其中的this就指向该实例 p,而实例p在初始化的时候被赋予number值为30,因此最后会输出“30”。
通过call()函数、apply()函数、bind()函数可以改变函数执行的主体,如果函数中存在this关键字,则this也将会指向call()函数、apply()函数、bind()函数处理后的对象
// 全局变量
var value = 10;
var obj = {
value: 20
};
// 全局函数
var method = function () {
console.log(this.value);
};
method(); // 10
method.call(obj); // 20
method.apply(obj); // 20
var newMethod = method.bind(obj);
newMethod(); // 20
函数的this变量只能被自身访问,其内部函数无法访问。因此在遇到闭包时,闭包内 部的this关键字无法访问到外部函数的this变量
varuser={
sport: 'basketball',
data: [
{name: "kingx1", age: 11},
{name: "kingx2", age: 12}
],
clickHandler: function () {
// 此时的this指向的是user对象
this.data.forEach(function (person) {
console.log(this); // [object Window]
console.log(person.name + ' is playing ' + this.sport);
})
}
};
这样最终会输出下面的结果。具体步骤和原因应该不需要多解释了。
kingx1 is playing undefined
kingx2 is playing undefined
那么如果我们希望forEach循环结果输出的sport值为“basketball”,应该怎么做呢?可以使用临时变量将clickHandler()函数的this提前进行存储,对其使用user对象,而在匿名函数中,使用临时变量访问sport属性,而不是直接用this访问。
var user = {
sport: 'basketball',
data: [
{name: "kingx1", age: 11},
{name: "kingx2", age: 12}
],
clickHandler: function () {
// 使用临时变量_this保存this
var _this = this;
this.data.forEach(function (person) {
// 通过_this访问sport属性
console.log(person.name + ' is playing ' + _this.sport);
})
}
};
user.clickHandler();
这样修改后的结果就是下面这样了
kingx1 is playing basketball
kingx2 is playing basketball
function f(k) {
this.m = k;
return this;
}
var m = f(1);
var n = f(2);
console.log(m.m);
console.log(n.m);
代码很短但是理解起来有一定难度。
在执行f(1)的时候,因为f()函数的调用没有所属对象,所以this指向window,然后this.m=k语句执行后,相当于window.m = 1。通过return语句返回“window”,而又将返回值“window”赋值给全局变量m,因此变成了window.m = window,覆盖前面的window.m = 1。
在执行f(2)的时候,this同样指向window,此时window.m已经变成2,即 window.m = 2,覆盖了window.m = window。通过return语句将window对象返回并赋值给n,此时window.n=window。
先看m.m的输出,m.m=(window.m).m,实际为2.m,2是一个数值型常量,并不存在m属性,因此返回“undefined”。再看n.m的输出,n.m=(window.n).m=window.m=2,因此输出“2”。
在JavaScript中,每个函数都包含两个非继承而来的函数apply()和call(),这两个函数的作用是一样的,都是为了改变函数运行时的上下文而存在的,实际就是改变函数体内this的指向。
bind()函数也可以达到这个目的,但是在处理方式上与call()函数和apply()函数有一定的区别。
call()函数调用一个函数时,会将该函数的执行对象上下文改变为另一个对象,语法为:
function.call(thisArg, arg1, arg2, ...)
// 定义一个add()函数
function add(x, y) {
return x + y;
}
// 通过call()函数进行add()函数的调用
function myAddCall(x, y) {
// 调用add()函数的call()函数
return add.call(this, x, y);
}
console.log(myAddCall(10, 20)); //输出“30”
apply()函数的作用域与call()函数是一致的,只是在传递参数的形式上存在差别,语法为:
function.apply(thisArg, [argsArray])
同样是上面call()的例子
//定义一个add()函数
function add(x, y) {
return x + y;
}
// 通过apply()函数进行add()函数的调用
function myAddApply(x, y) {
// 调用add()函数的apply()函数
return add.apply(this, [x, y]);
}
console.log(myAddApply(10, 20)); //输出“30”
bind()函数创建一个新的函数,在调用时设置this关键字为提供的值,在执行新函数时,将给定的参数列表作为原函数的参数序列,从前往后匹配。语法为:
function.bind(thisArg, arg1, arg2, ...)
事实上,bind()函数与call()函数接收的参数是一样的。其返回值是原函数的副本,并拥有指定的this值和初始参数。
同样是上面call()和apply()的例子
//定义一个add()函数
function add(x, y) {
return x + y;
}
// 通过bind()函数进行add()函数的调用
function myAddBind(x, y) {
// 通过bind()函数得到一个新的函数
var bindAddFn = add.bind(this, x, y);
// 执行新的函数
return bindAddFn();
}
console.log(myAddBind(10, 20)); //输出“30”
相同点:
不同点:
本节涉及到三个函数可以应用的一些具体算法或场景
Array数组本身是没有max()函数和min()函数的,所以可以使用apply()函数来改变 Math.max()函数和Math.min()函数的执行主体,然后将数组作为参数传递给Math.max()函数和Math.min()函数
var arr = [3, 5, 7, 2, 9, 11];
// 求数组中的最大值
console.log(Math.max.apply(null, arr)); // 11
// 求数组中的最小值
console.log(Math.min.apply(null, arr)); // 2
函数的参数对象arguments是一个类数组对象,自身不能直接调用数组的方法,但是我们可以借助call()函数,让arguments对象调用数组的slice()函数,从而得到一个真实的数组,后面就能调用数组的函数。
var arr = Array.prototype.slice.call(arguments);
下一章就会涉及到继承,其中的构造继承就会用到call()函数
假如存在这样一个场景,有一个数组,数组中的每个元素是一个对象,对象是由不同的属性构成,现在我们想要调用一个函数,输出每个对象的各个属性值。
我们可以通过一个匿名函数,在匿名函数的作用域内添加print()函数用于输出对象的各个属性值,然后通过call()函数将该print()函数的执行主体改变为数组元素,这样就可以达到目的了。
var animals = [
{species: 'Lion', name: 'King'},
{species: 'Whale', name: 'Fail'}
];
for (var i = 0; i < animals.length; i++) {
(function (i) {
this.print = function () {
console.log('#' + i + ' ' + this.species + ': ' + this.name); };
this.print();
}).call(animals[i], i);
}
在上面的代码中,在call()函数中传入animals[i],这样匿名函数内部的this就指向animals[i],在调用print()函数时,this也会指向animals[i],从而能输出speices属性和name属性。
在默认情况下,使用setTimeout()函数时,this关键字会指向全局对象window。当使用类的函数时,需要this引用类的实例,我们可能需要显式地把this绑定到回调函数以便继续使用实例。
此处之后再详细记录