01、JS函数基础
1.1、函数定义
函数(方法)就是一段定义好的逻辑代码,函数本身也是一个object
引用对象。三种函数构造方式:
🔸① 函数申明:function 函数名(参数){代码}
,申明函数有函数名提升的效果,先调用,后申明(和var申明提升类似,比var提升更靠前)。
🔸② 函数表达式:var func = function(参数){代码}
,定义变量指向函数,函数不需要命名。不过也可以像申明函数一样指定函数名,在其内部调用自己。
🔸③ Function构造函数:new Function(参数,代码)
,支持多个构造参数,前面为函数参数,最后一个为函数体,使用不多。
function func1(a) {
console.log(a);
}
var func2 = function(b){
console.log(b);
}
var func3 = new Function('c','console.log(c)');
//调用函数
func1(1);
func2(2);
func3(3);
🔸return 返回值:
- 通过
return
返回值,并结束方法。 - 无
return
,则默认返回undefined
。
1.2、参数argument
- 参数可以不传,则为
undefined
,也可多传,没卵用(也不一定,arguments
参数数组可以用)。 - 参数不可同名,如果同名,则后面为准。
- 形参与实参:函数定义的参数
a
为形参,调用时传入的数据3
为实参。 - 参数设置默认值几种方式:形参赋默认值(ES6)、参数验证赋值。
function func1(a="默认值") { //一般推荐的方式
a=a?a:"默认值"; //参数为null、undefined、0、false、空字符值都会用默认值
a=a||"默认值"; //同上
console.log(a);
}
- 参数的值传递和引用传递:取决于参数的类型,值类型传递副本,引用类型传递对象指针地址,操作的是同一个对象。这里需要理解JS里面的值类型、引用类型的基本原理。
function f1(n) {
n += 1;
}
let n = 100;
f1(n); //值传递:传入的是n的值副本,不影响原本n值
console.log(n); //100 n没有变
function f2(obj) {
obj.n += 1;
}
let nobj = { n: 100 };
f2(nobj); //引用传递,传入的是nobj地址指针,操作同一对象,对象是共享的
console.log(nobj); //{n: 101}
- arguments:函数传入的实参都保存在
arguments
数组对象中,对于任意数量参数的方法就很有用。
function sum() { //求和
let n = 0;
for (let i = 0; i < arguments.length; i++) {
n += arguments[i];
}
return n;
}
sum(1,2,3,108,594); //求和,支持任意个参数
- 剩余参数(...theArgs):把不确定的参数放到一个数组里,前面是确定参数,最后一个形参以
...
开头表示其他剩余参数数组,既然是数组,当然就可以支持任意数量的参数了。
// 连接字符串,支持任意数量字符参数
function connect(separator, ...sargs) {
let str = "";
for (let i = 0; i < sargs.length; i++) {
str += sargs[i]?.toString() + separator;
}
return str.slice(0, -separator?.toString().length); //去掉结尾的分隔符
}
connect('--', 1, 2, 3, 'a', 'b', 'c'); //1--2--3--a--b--c
1.3、函数(变量)作用域
- 局部变量:函数内的变量称为“局部变量”,函数里才有作用域的问题——不能被全局、其他函数访问。
- 全局变量:全局变量可以被自由访问,包括函数内
- 父函数变量:函数中定义的函数也可以访问在其父函数中定义的所有变量,和父函数有权访问的任何其他变量。简单来说就是函数变量作用域单向向下传递,子级可以访问父级的变量。
var num1 = 0;
function getScore() {
let num1 = 2, //和全局变量同名
num2 = 3;
function add() {
num3 = 5; //没用任何申明的局部变量,使用后自动变为隐式全局变量
return num1 + num2; //可以访问全局、父级的变量
}
return add();
}
getScore(); //5
console.log(num1, num3); //0 5
⁉️注意:
- 全局、局部(方法内)变量同名,两者没有关系,函数内肯定就近用自己的了。
- 没用任何申明的局部变量,使用后自动变为隐式全局变量,全局
window
莫名其妙就有了很多私生子,so,不要这样干!
1.4、()=>{ }箭头函数
箭头函数(IE🚫)是一种简洁的函数表达式,顾名思义就是用箭头=>
创建函数,它没有自己的this
、arguments
、super
。箭头函数总是匿名的,适用于那些需要匿名函数的地方。
let add1 = (a, b) => { return a + b; };
let add = (a, b) => a + b; //同上,只有一行代码可以省略花括号{} 和 return
let logDate = () => { console.log(new Date()) }; //可以没有参数
let logError = e => { console.log('发生错误:', e) }; //一个参数可以省略括号
=>
箭头函数和普通函数有什么区别呢?这是考点:
// 箭头函数
let f1 = (a, b) => {
console.dir(f1.prototype); //undefined
console.log(arguments); //arguments is not defined
}
// 普通函数,嵌套了一个箭头函数
let f2 = function (a) {
console.dir(f2.prototype); //Object
// 嵌套的箭头函数
let f1 = (a, b) => {
console.log(arguments);
} //是父级f2的arguments
f1(a, 2);
}
//this
var name = '大哥';
let user = {
name: 'sam',
sayHi1: function () {
console.log('Hi', this.name)
},
sayHi2: () => { console.log('Hi', this.name) },
}
user.sayHi1(); //Hi sam :this指向调用者
user.sayHi2(); //Hi 大哥 :this指向定义的环境,全局对象
// 指定新的this
let nobj={name:'张三'};
user.sayHi1.call(nobj); //Hi 张三 :this指向绑定对象
user.sayHi2.call(nobj); //Hi 大哥 :this指向全局对象,始终没变
1.5、全局函数
let arr = eval('[1,2,3]'); //转换字符串为数组
let jobj = eval("({name:'sam',age:22})"); //转换字符串为JSON对象
let jobj2 = new Function("return {name:'sam',age:22}")(); //转换字符串为JSON对象
//计时time,需一个统一标志
console.time("load");
console.timeLog("load"); //load: 5860ms
console.timeLog("load"); //load: 18815ms
console.timeEnd("load"); //load:25798 毫秒 - 倒计时结束
02、函数调用/call/apply/bind
常用的函数调用方式:
- 直接函数名调用:
函数名(参数...);
- 对象调用:对象里的函数,
对象.函数名(参数...);
- 递归调用,嵌套调用自身,须注意退出机制,避免死循环,代码的世界没有天荒地老。
不常用函数调用方式:call/apply/bind 调用函数都可以指定this
值。
🔸call()
通过 Function.prototype.call() 调用一个函数,调用语法:
- 第一个参数
thisArg
指定运行时this
,当第一个参数为null、undefined的时候,默认指向window。这一点可用来实现函数的“继承”(在构造函数中调用父构造函数) - 后面为函数原本的参数。
function sum(n1, n2) {
return n1 + n2;
}
sum(1,2); //正常调回
sum.call(null, 1, 2); //call调用
console.log(Math.max(1, 2)); //正常调回
console.log(Math.max.call(null, 1, 2)); //call调用
🔸apply()
通过 Function.prototype.apply() 调用函数,调用语法:
和call()
的唯一区别就是第二个参数是一个参数数组,数组参数会分别传入原函数。
function sum(n1, n2) {
return n1 + n2;
}
sum.apply(null, [1, 2]); //apply调用
console.log(Math.max.apply(null, [5, 4, 2, 1, 22, 9])); //apply调用,数组传入多个参数
//绑定this
var uname = "sam";
let f = function () {
console.log(this.uname);
}
f(); //sam
f.call({ uname: "call" }); //call
f.apply({ uname: "apply" }); //apply
🔸bind()
通过 Function.prototype.bind() 创建一个副本函数:该副本函数绑定了this
和参数,且一经绑定,永恒不变(不可更改,不可二次绑定)。
function log(type, title, message) {
console.log(`type:${type}, title:${title}, message:${message}`)
}
let errorLog = log.bind(null, '错误'); //返回绑定的函数,绑定第一个参数
errorLog('登录发生异常', '超过登录次数');
03、函数闭包
3.1、什么是闭包?
闭包是函数和申明该函数的词法环境的组合,简单来说能够访问其他函数(通常是父函数)内部变量的函数,加上他引用的外部变量,组成了闭包。通常就是嵌套函数,嵌套函数可以”继承“父函数的参数和变量,或者说内部函数包含外部函数的作用域。
- 内部函数+外部引用形成了一个闭包:它可以访问外部函数的参数和变量。闭包存储了自己和其作用域的变量,这样在函数调用栈上才能使用外部函数的变量。
function FA(x) {
function FB(y) {
function FC(z) {
console.log(x + y + z);
}
FC(3);
console.dir(FC)
}
FB(2);
console.dir(FB)
}
FA(1); //6
console.dir(FA)
作用域链(C>B>A):B和A形成闭包,B可以访问A,保存了A的变量;C和B形成闭包,可以访问B(也包括B有的A作用域),如下图FC函数形成的闭包中存储了FB、FA的变量、参数信息。
因此,闭包就是为了解决了函数的词法作用域问题,FC函数就可以单独使用了。V8引擎是把闭包封装成了一个"Closure"
对象,保存函数上下文中的[[Scope]]
集合里。同一个函数多次调用都会产生单独的闭包,如果闭包使用不当或太多,容易引发内存泄漏风险。
3.2、闭包应用:柯里化(Currying)
柯里化是一种函数的高级玩法,简单来说,就是把多参数的函数f(a,b)
,转换成了另一种函数形式 f(a)(b)
。
//一个普通的日志函数
function print(title, message) {
console.log(title, message);
}
//柯里化转化函数
function curry(func) {
return function (title) {
return function (message) {
return func(title, message);
}
}
}
//柯里化转化
let cprint = curry(print);
//调用
cprint('用户模块')('用户1登录了');
//复用:复用包含了title参数值的函数。
let userPrint = cprint('用户模块');
userPrint('用户1退出登录');
userPrint('用户3打赏了游艇');
原理其实不难理解,就是利用闭包的(词法作用域)机制,返回多层闭包函数,直到最后一个参数来了才执行。这么做到底有什么好处?——答案就是复用,复用参数值。如上面示例中的userPrint
,是一个包含了title
参数值的(闭包)函数,他还有一个正式的名字,叫偏函数。
一个更通用的柯里化实现如下,采用递归的方式,不仅可以生成任意偏函数,还支持正常调用。
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
//使用
let log = curry(print);
log('登录模块','系统崩了'); //正常调用
let userLog = log('直播模块'); //偏函数-复用
userLog('表演才艺');
userLog('上链接');
04、this关键字
this 是JS的关键字,指向当前执行的环境对象,允许函数内引用当前环境的其他变量,不同场景指向不同。在严格模式("use strict";
)下又会不同。大多数情况下,函数的调用方式决定了 this
的值(运行时绑定),每次调用函数的this
也可能不同。
this.x1 = 100;
console.log(this.x1); //这里的this指向全局对象window
function User() {
this.sname = "sam";
this.age = 20;
console.log(this.sname, this.age);
}
new User(); //sam 20,User构造函数里的this指向new创建的新User实例
- this=全局window:在全局执行环境中(在任何函数体外部)this 都指向全局对象
window
。 - this=new对象:构造函数中的
this
指向其新对象;对象的属性方法中的this
指向该对象。 - this=调用者:局部(函数内的)this,谁调用函数,this指的就是谁。箭头函数除外,箭头函数本身没有this,也不会接收call、apply的传递,指向其函数定义环境的
this
,而非执行时。 - this=事件元素:在事件中,this表示接收事件的元素。
- this=绑定对象:call(thisArg)、apply(thisArg)、bind(thisArg)绑定参数
thisArg
作为其上下文的this,若参数不是对象也会被强制转换为对象,强扭的瓜解渴! - this=undefined:严格模式下,如果this没有被执行环境(execution context)定义,那
this
就是undefined
。
function Foo() {
console.log(this);//调用Foo(),this指向window;如果new Foo()则指向新对象
var fa = () => { console.log("fa:" + this) };
var fb = function () {
console.log("fb:" + this);
}
fa(); //箭头函数,调用Foo(),this指向调用者window;如果new Foo()则指向新对象
fb(); //匿名函数,调用Foo()、构造函数调用,this指向调用者window,
this.fc1 = fa; //属性方法:this指新对象
}
Foo(); //调用Foo()函数
new Foo(); //构造函数调用创新实例