函数的定义方式
函数声明
function foo() {}
foo();
函数表达式
var foo = function() {};
foo();
-
下面是一个根据条件定义函数的例子,在不同浏览器中结果不一致。函数声明放在if-else的语句中,在IE8浏览器中会出现问题
if (true) { function f () { console.log(1); } } else { function f () { console.log(2); } }
不过可以使用函数表达式解决,所以以后用函数表达式,不用函数声明:
var f; if (true) { f = function() { console.log(1); } } else { f = function() { console.log(2); } }
函数声明与函数表达式的区别
- 函数声明必须有名字
- 函数声明会提升函数,在预解析阶段就已创建,声明前后都可以调用
- 函数表达式类似于变量赋值
- 函数表达式可以没有名字,例如匿名函数
- 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用
函数的调用方式
-
普通函数:
方法名();
function f1() { console.log("文能提笔控萝莉"); } f1();
-
构造函数:
new
function F1() { console.log("我是构造函数,我骄傲"); } var f = new F1();
-
对象方法:
对象.方法名();
function Person() { this.play = function () { console.log("玩代码"); }; } var per = new Person(); per.play();
函数内 this
指向的不同场景
函数的调用方式决定了 this
指向的不同,这就是对函数内部 this 指向的基本整理,代码写多了自然就熟了
哪里的this | 是谁 | 说明 |
---|---|---|
普通函数中 | window | 严格模式下是undefined |
定时器中 | window | |
构造函数中 | 实例对象 | |
对象.方法中 | 该方法所属对象 | |
原型对象方法中 | 实例对象 | |
事件绑定方法 | 绑定事件对象 |
函数也是对象
-
对象中有
__proto__
原型,是对象;函数中有prototype
原型,是对象 -
结论:所有的函数都是
Function
构造函数创建出来的实例对象var f1 = new Function("num1", "num2", "return num1+num2");
- 对象与函数的标志:
__proto__
是对象;prototype
是函数
- 函数是对象,对象不一定是函数,如Math中有
__proto__
,但是没有prorotype
严格模式
-
语法:在函数前边加上
"use strict;"
-
示例:
"use strict"; function f1() { console.log(this); } f1(); // strict下为undefined,普通模式正确 window.f1(); // strict模式下正确,普通模式正确
call、apply、bind
了解了函数this指向的不同场景之后,我们知道有些情况下为了使用某种特定环境的 this 引用,这时候我们就需要采用一些特殊手段来处理,例如经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。然而实际上对于这种做法JavaScript为我们专门提供了一些函数方法用来更优雅的处理函数内部 this 指向问题
call和apply
- 作用:改变this的指向,传入哪个对象就指向谁
- 参数:
apply(对象, [参数0, 参数1...])
call(对象, 参数0, 参数1...)
- 返回值:调用
apply
和call
的返回值就是原函数的返回值,可用变量接收 - 适应性:想使用别的对象的方法,并且希望这个方法是当前对象的。
- 注意:
apply
和call
若没传参或传null
,那么调用该方法的函数对象中的this
就是默认的window
apply
和call
方法也是函数的调用方式,即f()
和f.apply()
和f.call()
等效apply
和call
都可让函数来调用,传入参数和函数自己调用的写法不同,但效果一样apply
和call
方法实际上并不在函数这个实例对象中,而是在Function
的prototype
中(实例对象调用方法,方法要么在实例对象中存在,要么在原型对象中存在)
- 区别:
call
方法接受的是若干个参数的列表,而apply()
方法接受的是一个包含多个参数的数组
call
call
方法调用一个函数,其具有一个指定的this
值和参数列表- 语法:
fun.call(thisArg[, arg1[, arg2[, ...]]]);
- 参数:
thisArg
:在 fun 函数运行时指定的this
值,如果指定了null
或undefined
则内部this
指向window
arg1, arg2, ...
:参数列表
apply
-
apply
方法调用一个函数,其具有一个指定的this
值,以及作为一个数组提供的参数 -
语法:
fun.apply(thisArg, [argsArray])
-
参数:和
call
类似,例如fun.apply(this, ['eat', 'bananas'])
bind
-
bind
函数会创建一个新函数(称为绑定函数),新函数与被调函数(目标函数)具有相同的函数体(在 ECMAScript 5规范中内置的call属性)。当目标函数被调用时 this 值绑定到bind()的第一个参数,该参数不能被重写。绑定函数被调用时,bind()也接受预设的参数提供给原函数。 -
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数
-
作用:复制函数,并改变this的指向
-
语法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
// 参数可以在复制的时候传进去 // 定义 var 新名 = 原名.bind(对象, 参数0, 参数1...); // 调用 新名(); // 也可在复制之后调用时传进去 // 定义 var 新名 = 原名.bind(对象); // 调用 新名(参数0,参数1...);
-
参数:
thisArg
:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new操作符调用绑定函数时,该参数无效。arg1, arg2, ...
:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法
-
返回值: (复制之后的)函数,即由指定的this值和初始化参数改造的原函数拷贝
-
注意:
apply
和call
是调用的时候改变this指向;bind
是复制一份时改变了this
指向- 参数可以在复制的时候传进去,也可在复制之后调用时传进去
- 对象不传或传入
null
时,this
指向window
-
示例:
this.x = 9; var module = { x: 81, getX: function() { return this.x; } }; module.getX(); // 返回 81 var retrieveX = module.getX; retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域 // 创建一个新函数,将"this"绑定到module对象 // 新手可能会被全局的x变量和module里的属性x所迷惑 var boundGetX = retrieveX.bind(module); boundGetX(); // 返回 81
function LateBloomer() { this.petalCount = Math.ceil(Math.random() * 12) + 1; } // Declare bloom after a delay of 1 second LateBloomer.prototype.bloom = function() { window.setTimeout(this.declare.bind(this), 1000); }; LateBloomer.prototype.declare = function() { console.log('I am a beautiful flower with ' + this.petalCount + ' petals!'); }; var flower = new LateBloomer(); flower.bloom(); // 一秒钟后, 调用'declare'方法
function ShowRandom() { this.number = parseInt(Math.random() * 10 + 1); // 随机数 } ShowRandom.prototype.show1 = function() { // 添加原型方法 // 改变定时器中的this的指向,window -> 实例对象 window.setInterval(this. show2.bind(this), 1000); }; ShowRandom.prototype.show2 = function() { // 添加原型方法,显示随机数 console.log(this.number); }; var sr = new ShowRandom(); // 实例对象
总结
-
call和apply特性一样
- 都是用来调用函数,而且是立即调用
- 可以在调用函数的同时,通过第一个参数指定函数内部
this
的指向 - call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递
- apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
- 如果第一个参数指定了
null
或undefined
则内部 this 指向window
-
bind
-
可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
-
它和 call、apply 最大的区别是:bind 不会调用
-
bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
- 在 bind 的同时,以参数列表的形式进行传递
- 在调用的时候,以参数列表的形式进行传递
- 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
- 两者合并:bind 时传递的参数和调用时传递的参数会合并到一起,传递到函数内部
-
函数的其它成员
arguments
:实参数组caller
:函数的调用者,f1函数在f2函数中被调用,caller
就是f2length
:形参的个数name
:函数的名称,只读不能修改
function fn(x, y, z) {
console.log(fn.length); // 形参的个数
console.log(arguments); // 伪数组实参参数集合
console.log(arguments.caller === fn); // 函数本身
console.log(fn.caller); // 函数的调用者
console.log(fn.name); // 函数的名字
}
function f() {
fn(10, 20, 30);
}
f();
高阶函数
作为参数
-
例子:
// fn是参数,最后作为函数使用 function f1(fn) { console.log("f1..."); fn(); // fn当成一个函数使用 } // 传入匿名函数 f1(function () { console.log("匿名函数..."); }); // 传入命名函数 function f2() { console.log("f2..."); } f1(f2);
-
应用:
var arr = [1, 100, 20, 200, 40, 50, 120, 10]; // 排序(不稳定,很可能没排序) arr.sort(); console.log(arr); // 排序(稳定) // 函数作为参数使用,匿名函数作为sort方法的参数使用,此时匿名函数有两个参数 arr.sort(function(obj1, obj2) { if (obj1 > obj2) { return 1; // 1 -> 正序;-1 -> 倒序 } else if(obj1 === obj2) { return 0; } else { return -1; // -1 -> 正序;1 -> 倒序 } }); console.log(arr); // 稳定
作为返回值
-
获取某个对象的数据类型的样子:
// 判断这个对象是不是某个类型的 console.log(obj instanceof Object); // 获取某个对象的数据类型的样子 Object.prototype.toString.call(对象);
-
判断某个对象的类型是不是传入的类型:
// [10,20,30] 是不是"[object Array]" // type-是变量-是参数-"[object Array]" // obj-是变量-是参数-[10,20,30]; function getFunc(type) { return function (obj) { return Object.prototype.toString.call(obj) === type; } } var isArray = genFun('[object Array]') var isObject = genFun('[object Object]') console.log(isArray([1, 2, 4])) // true console.log(isObject({})) // true
JS预解析
JS执行过程
JavaScript 运行分为两个阶段:先预解析全局作用域,然后执行全局作用域中的代码,在执行全局代码的过程中遇到函数调用就会先进行函数预解析,然后再执行函数内代码
- 预解析
- 全局预解析(所有变量和函数声明都会提前;同名的函数和变量函数的优先级高)
- 函数内部预解析(变量、函数、形参)
- 执行
预解析
-
定义:变量使用或函数调用时,会把变量或函数【声明】提升到作用域最上面。
-
注意:预解析只将声明提前,赋值不会被提前。
// var num; console.log(num); // undefined var num = 10;
// var num; f1(); var num = 20; function f1() { console.log(num); // undefined }
// function a(){console.log('哈哈');}; console.log(a); // 函数a代码 function a() { // 注意:这相当于变量a的声明 console.log('哈哈'); } var a = 1; console.log(a); // 1
// var a(局部变量); a=9; b=9(隐式全局); c=9(隐式全局); f1(); console.log(c); // 9 console.log(b); // 9 console.log(a); // 报错 function f1() { // var a; var a = b = c = 9; console.log(a); // 9 console.log(b); // 9 console.log(c); // 9 }
// var f1; f1(); // f1... function f1() { console.log('f1...'); } f2(); // Uncaught TypeError: f2 is not a function var f2 = function() { console.log('f2...'); };
// var fn; fn(); // undefined function fn() { // var ab; console.log(ab); var ab = 1; }
函数闭包
-
概念:函数A中有函数B,函数B可访问函数A中定义的变量,此时形成了闭包。闭包就是能够读取其他函数内部变量的函数,由于在js中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
-
分类:函数模式的闭包、对象模式的闭包。
-
作用:缓存数据,延长作用域链,在函数外部读取函数内部成员让函数内成员始终存活在内存中。优点同时也是缺陷,没有及时释放。
-
适应性:如果想要缓存数据,就把这个数据放在外层的函数和里层的函数的中间位置。
// 函数模式的闭包:在函数f1()有一个函数f2()访问f1的变量。 function f1(){ var num = 1; function f2(){ console.log(num); } f2(); } f1(); // 对象模式的闭包: 函数f3()中有一个对象obj访问f3的变量。 function f3() { var num = 10; var obj = { age: num }; console.log(obj.age); // 10 } f3();
function fn() { var count = 0; return { showCount: function () { console.log(count); }, incrCount: function () { count++; } } } var fns = fn(); fns.showCount(); // 0 fns.incrCount(); fns.showCount(); // 1
闭包例子
var arr = [10, 20, 30];
for(var i = 0; i < arr.length; i++) {
arr[i] = function () {
console.log(i);
};
}
console.log(111);
for(var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
console.log(222);
示例3:投票
示例4:判断类型
示例5:沙箱模式
沙箱
-
黑盒环境,在一个虚拟的环境中模拟真实世界、做实验,实验结果和真实世界的结果是一样,但是不会影响真实世界。
-
示例:
var num = 100; (function () { // 沙箱:小环境内部的作用域 var num = 10; // 和全局的互不影响 console.log(num); // 10 }()); console.log(num); // 100
闭包思考题
var name = "The Window";
const object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()()); // The Window
- 在
getNameFunc
函数内部,返回了一个匿名函数,而匿名函数中使用了this
关键字来引用对象的属性name
。然而,由于匿名函数是作为全局函数调用的,因此this
关键字将指向全局对象,而不是object
对象。因此,匿名函数中的this.name
实际上指向了全局对象的name
属性,其值为 “The Window”
var name = "The Window";
const object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
}
};
console.log(object.getNameFunc()());
getNameFunc
函数返回了一个匿名函数,而在getNameFunc
函数内部,使用了变量that
来保存对外部this
的引用。在匿名函数中,使用了that.name
来获取对象的name
属性,因为that
指向了外部函数的this
,即指向了object
对象。因此,最终输出的结果是 “My Object”。
应用—缓存
-
对比:
// 普通函数 -> 不缓存 function f1() { var num = 10; num++; return num; } console.log(f1()); // 11 console.log(f1()); // 11 console.log(f1()); // 11 // 闭包 -> 缓存 function f2() { var num = 10; return function () { num++; return num; } } var ff = f2(); console.log(ff()); // 11 console.log(ff()); // 12 console.log(ff()); // 13
-
相同的随机数:
// 产生3个随机数,但都是不同的,因为数据没缓存 function showRandom() { var num = parseInt(Math.random()*10+1); console.log(num); } showRandom(); showRandom(); showRandom(); // 闭包的方式,产生三个随机数,但是都是相同的 function f1() { var num = parseInt(Math.random() * 10 + 1); return function () { console.log(num); } } var ff = f1(); ff(); ff(); ff();
函数递归
递归执行模型
function fn1 () {
console.log(111);
fn2();
console.log('fn1');
}
function fn2 () {
console.log(222);
fn3();
console.log('fn2');
}
function fn3 () {
console.log(333);
fn4();
console.log('fn3');
}
function fn4 () {
console.log(444);
console.log('fn4');
}
fn1();
例子
// 计算阶乘的递归函数
function factorial (num) {
if (num <= 1) {
return 1;
}
return num * factorial(num - 1);
}
// 求前n项和
function getSum(n){
if(n == 1) {
return 1;
}
return n + getSum(n-1);
}
// 求各位数字的和
function getEverySum(x){
if(x < 10){
return x;
}
// 返回个位数的值+其他位数的值
return x % 10 + getEverySum(parseInt(x/10));
}
// 求斐波那契数列
function getFib(n){
if(n==1 || n==2){ // 如果到了1或2项,返回1
return 1;
}
// 否则返回前1项+前2项的和
return getFib(n-1) + getFib(n-2);
}