JavaScript入门②-函数(1)基础{浅出}

时间:2022-12-01 11:08:32

JavaScript入门②-函数(1)基础{浅出}

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);

❗注意:JS中没有方法重载,不允许相同的函数名,重名会被覆盖。

????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????)是一种简洁的函数表达式,顾名思义就是用箭头=>创建函数,它没有自己的thisargumentssuper。箭头函数总是匿名的,适用于那些需要匿名函数的地方。

语法规则:(param1, param2, …, paramN) => { ... }

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) }; //一个参数可以省略括号

=>箭头函数和普通函数有什么区别呢?这是考点:

区别 描述
申明方式不同 普通函数需要function关键字,箭头函数当然就是箭头=>
没有自己的arguments 箭头函数在全局环境中,没有arguments;当箭头函数处于普通函数的中,arguments是上层普通函数的arguments
没有自己的this 没有自己的this,其this指向其函数定义的外层作用域环境的this,且不能被call、apply、bind函数改变。
没有自己的prototype 箭头函数没有自己的原型,加上没有自己的this,所以也就不能作为构造函数使用
// 箭头函数
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、全局函数

函数 描述
eval() 执行JS代码(不推荐):eval("console.log('eval')");
- 比较危险,它使用与调用者相同的权限执行代码,字符串代码容易被被恶意修改。
- 效率低,它必须先调用JS解释,也没有其他优化,还会去查找其他JS代码(变量)。
- 推荐用 Function() 构造函数代替eval()
isNaN() 判断一个值是否是非数值
parseFloat() 转换字符为浮点数
parseInt() 转换字符为整数
decodeURI() URL解码
encodeURI() URL编码
alert(str) 弹窗消息提示框
confirm(str) 弹窗消息询问确认框,返回boolean
console 控制台输出
console.log(str) 控制台输出一条消息
console.error(str); 打印一条错误信息,类似的还有infowarn
console.dir(object) 打印对象
console.trace() 输出堆栈
console.time(label) 计时器,用于计算耗时(毫秒):time(开始计时) > timeLog(计时) > timeEnd(结束)
console.clear() 清空控制台,并输出 Console was cleared。
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() 调用函数,指定this、参数 function.call(thisArg, arg1, arg2, ...)
apply() 调用函数,指定this、参数数组 function.apply(thisArg, argsArray)
bind() 绑定(复制)一个函数,指定this、参数 function.bind(thisArg, arg1, arg2, ...)

????call()

通过 Function.prototype.call() 调用一个函数,调用语法:

function.call(thisArg, arg1, arg2, ...)

  • 第一个参数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() 调用函数,调用语法:

function.apply(thisArg, argsArray)

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.bind(thisArg[, arg1[, arg2[, ...]]])

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的变量、参数信息。

JavaScript入门②-函数(1)基础{浅出}

因此,闭包就是为了解决了函数的词法作用域问题,FC函数就可以单独使用了。V8引擎是把闭包封装成了一个"Closure"对象,保存函数上下文中的[[Scope]]集合里。同一个函数多次调用都会产生单独的闭包,如果闭包使用不当或太多,容易引发内存泄漏风险。

JS设计闭包这个东西,一言难尽!详见后续《函数执行上下文》

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 都指向全局对象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();  //构造函数调用创新实例

又是一个JS的坑!好像懂了,又好像没懂!详见后续


©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀