Skip to content

函数 ​

函数的作用 ​

  • 主要用来储存一段代码,

  • 代码重复使用,大量减少代码量

函数声明及调用 ​

  • 普通声明:function name(){content}

  • 表达式声明: var fn = function(){countent},

  • 函数调用:name();

  • 函数的 length 属性,为形参的个数;

函数注意事项 ​

  • 函数调用时,是不分先后的;

  • 在任何地方给一个已经声明且赋值的变量再重新声明,重新声明都是无效的。

  • 函数重复声明时,后面的函数会覆盖前面的函数;

  • 如果先用函数表达式声明的,后面声明的普通函数是不能覆盖前面的函数,如果前面是普通声明,后面是表达式声明,则照常执行;

JS
var x = function () { alert(1) };
x();    //1
function x() { alert(3) };
x();     //1


function x() { alert(3) };
x();     //3
var x = function () { alert(1) };
x();    //1
var x = function () { alert(1) };
x();    //1
function x() { alert(3) };
x();     //1


function x() { alert(3) };
x();     //3
var x = function () { alert(1) };
x();    //1

面试题:

js
var x = 1
var y = 0
var z = 0
function add(n) {
  return (n = n + 1)
}
y = add(x)
function add(n) {
  return (n = n + 3)
}
z = add(x)
s = y + z
var x = 1
var y = 0
var z = 0
function add(n) {
  return (n = n + 1)
}
y = add(x)
function add(n) {
  return (n = n + 3)
}
z = add(x)
s = y + z

y 和 z 都是 4,后面定义的影响到前面定义的;

函数放在其他位置 ​

  1. 对象中的函数时

    js
    var aa = {
      name: '张三',
      fn: function () {
        循环体
      },
    }
    aa.fn()
    var aa = {
      name: '张三',
      fn: function () {
        循环体
      },
    }
    aa.fn()

    经常使用于函数过多,变量名重复;

  2. 数组中的函数时

    js
    var ary = [1, 2, function () {}]
    // 调用:ary[3]()
    var ary = [1, 2, function () {}]
    // 调用:ary[3]()

全局函数 ​

JavaScript 中包含以下 7 个全局函数,用于完成一些常用的功能:

escape( )、eval( )、isFinite( )、isNaN( )、parseFloat( )、parseInt( )、unescape( )。

注意:setTimeout 不是 js 的全局函数,是 window 的全局函数

封装函数 ​

  1. 写出主体代码;

  2. 找出可变的变量,用形参代替;

  3. 放到函数中;

  4. 调用函数并检查;

事件处理函数 ​

obox.onclick = aa;

事件处理函数当中,函数后面不需要跟小括号;

函数参数的类型: ​

  1. 形参和实参

    • 创建:function 函数名(形参){} 多个形参用逗号隔开;

    • 调用:函数名(实参);

    注意:

    • 形参比实参多,多出来的形参值为 undefined;

    • 实参比形参多,多出来的实参不参与计算,等于没有被识别;

  2. arguments

    它是一个对象,是一个类数组(有长度,可以循环,不能使用数组的方法);

    类似于数组,数据由实参决定,arguments 是函数的内置对象;

    当使用一个函数不知道这个函数可以使用哪些形参时,可以利用 arguments 获取可以使用的形参;可以使用下标方式取值,比如:arguments[0];

    js
    //查询map函数有哪些形参可用;
    ary.map(function () {
      console.log(arguments)
    })
    //查询map函数有哪些形参可用;
    ary.map(function () {
      console.log(arguments)
    })

回调函数 ​

  1. 把函数作为实参传递到另一个函数当中,为回调函数;

  2. 回调函数是某个操作或某个动作做完以后调用的函数。

回调函数实例:

js
function add(a, b) {
  return a + b
}
function fn(num1, num2, func) {
  //这里fn用来传入一个函数;
  return func(num1, num2)
}
console.log(fn(10, 20, add)) //这里不能使用小括号,因为在fn函数里面已经调用小括号了;
function add(a, b) {
  return a + b
}
function fn(num1, num2, func) {
  //这里fn用来传入一个函数;
  return func(num1, num2)
}
console.log(fn(10, 20, add)) //这里不能使用小括号,因为在fn函数里面已经调用小括号了;

递归函数 ​

  1. 在函数中自己调用自己,也可以是一个 for 循环;

  2. 递归一定要有结束条件;

递归: 斐波那契数列第几个数是什么;

js
function fn(n){
	if(n===1 || n===2) return 1;        必须return 1个数值
	return fn(n-1) + fn(n - 2);
}


// 求n个数字的和;   递归函数自己调用自己;
function fn(n){
    if(n == 1) return 1;
    return n + fn(n-1)
}
console.log(3);
// 3 + fn(2)
// 3 + (2 + fn(1))
// 3 + (2 + (1))
function fn(n){
	if(n===1 || n===2) return 1;        必须return 1个数值
	return fn(n-1) + fn(n - 2);
}


// 求n个数字的和;   递归函数自己调用自己;
function fn(n){
    if(n == 1) return 1;
    return n + fn(n-1)
}
console.log(3);
// 3 + fn(2)
// 3 + (2 + fn(1))
// 3 + (2 + (1))

尾递归 ​

  1. 什么是尾调用:指某个函数的最后一步是调用另一个函数

    js
    // 这种情况叫尾调用
    function f(x) {
      return g(x)
    }
    
    // 这种情况不叫尾调用
    function f(x) {
      let y = g(x)
      return y
      // 调用函数g后还有别的操作,不叫尾调用;
    }
    // 情况二 也不叫尾调用
    function f(x) {
      return g(x) + 1 //调用后还有操作;
    }
    // 这种情况叫尾调用
    function f(x) {
      return g(x)
    }
    
    // 这种情况不叫尾调用
    function f(x) {
      let y = g(x)
      return y
      // 调用函数g后还有别的操作,不叫尾调用;
    }
    // 情况二 也不叫尾调用
    function f(x) {
      return g(x) + 1 //调用后还有操作;
    }
  2. 尾递归:函数调用自身,称为递归,如果尾调用自身,称为尾递归

    递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生栈溢出错误,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出错误;

    递归保存 n 个调用记录,时间复杂度:O(n)

    尾递归只保留一个调用记录,时间复杂度:O(1)

    js
    // 递归写法
    function fabonacci(n) {
      if (n === 1 || n === 2) return 1
      return fabonacci(n - 1) + fabonacci(n - 2)
      //一直在进行记录,数值过大栈溢出;
    }
    
    // 尾递归写法;
    function fabonacci(n, start = 1, total = 1) {
      if (n === 1 || n === 2) return total
      return fabonacci(n - 1, total, start + total)
    }
    /**
     * 尾递归斐波那契分析:
     * 第一个为次数,第二个为保留上一次的结果,第三个用于返回的结果;
     * 假如传入10,第一次为 9 1 2
     * 第二次为  8 2 3
     * 第三次为  7 3 5
     * 最后一位依次为:  2  3  5  8
     * 在es6中,只要使用尾递归,就不会发生栈溢出,相对节省内存;
     */
    
    function fib(n) {
      let p = 0,
        c = 1
      while (n--) [p, c] = [c, p + c]
      return p
    }
    // 递归写法
    function fabonacci(n) {
      if (n === 1 || n === 2) return 1
      return fabonacci(n - 1) + fabonacci(n - 2)
      //一直在进行记录,数值过大栈溢出;
    }
    
    // 尾递归写法;
    function fabonacci(n, start = 1, total = 1) {
      if (n === 1 || n === 2) return total
      return fabonacci(n - 1, total, start + total)
    }
    /**
     * 尾递归斐波那契分析:
     * 第一个为次数,第二个为保留上一次的结果,第三个用于返回的结果;
     * 假如传入10,第一次为 9 1 2
     * 第二次为  8 2 3
     * 第三次为  7 3 5
     * 最后一位依次为:  2  3  5  8
     * 在es6中,只要使用尾递归,就不会发生栈溢出,相对节省内存;
     */
    
    function fib(n) {
      let p = 0,
        c = 1
      while (n--) [p, c] = [c, p + c]
      return p
    }

作用域 ​

每个函数都有自己的执行期上下文,每个执行期上下文都有自己的变量对象;

全局变量的作用域 ​

  1. 用 var 声明的变量,可以在全局使用;

  2. 除了在函数内部定义的变量都是全局变量;

  3. 全局作用域声明的变量、函数会变成 window 的方法或属性。

  4. 全局变量如果页面不关闭,全局变量就不会释放,就会占空间,消耗内存,所以尽可能的在函数里面使用局部变量;

局部变量 ​

在函数内部定义的变量是局部变量,

块级作用域 ​

花括号里面声明的变量只能在花括号当中使用,比如:if 语句和 for 循环语句;

但是在 js 当中,外面也能使用花括号的变量,因为在 js 里没有块级作用域,只有函数除外,在 es6 里面 let 声明的变量有作用域;

隐式全局变量 ​

  1. 声明变量没有 var,就叫隐式全局变量;

  2. 如果在函数里面声明变量没有 var,函数执行之后,外面也能拿到函数里面的变量;

注意:

  • 全局变量是不能被删除的,隐式全局变量时可以被删除的;

  • 定义变量使用 var 是不能被删除的,没有 var 是可以删除的;

delete 变量 删除变量。

[[scope]] ​

(这里跳过,建议去看视频,推荐渡一教育的视频,作用域讲的比较详细)

作用域是因为由函数的产生而产生的作用域;

每个 javascript 函数也是一个对象,函数叫类对象,函数可以访问它的 name 属性和 prototype 属性,但有些属性是不能访问的,比如[[scope]],这些属性仅 javascript 引擎存取;

AO(Active Object)是执行期上下文,记录了该环境中所有声明的参数,变量和函数;

GO(Global Object)是全局产生的执行期上下文;

全称:VO(variable),AO 是正在执行的 VO,GO 全局中的 VO;

[[scope]]是隐式的属性,不能拿出来,我们不能访问,系统可以用,里面存储了执行期上下文(AO)的集合。

执行期上下文:

当一个函数执行时,会创建一个称为执行期上下文(AO)的内部对象,

一个执行期上下文定义了一个函数执行时的环境,

函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,

当函数执行完毕,它所产生的执行上下文被销毁;

查找变量,从作用域链的顶端依次向下查找;

作用域链:[[scope]]中存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;

每一个函数都有一个执行期上下文的集合,叫作用域链;

当一个函数被创建时,就可以访问fn.name,fn.[[scope]]

fn.[[scope]]里面存放了一个 0,0 指向 GO,global Object;

一个函数被定义时,可以访问[[scope]],里面储存的是 GO,一个函数执行之后会产生一个新的执行期上下文,存的是 AO,GO 被压下去了,变成了第 1 位,AO 在第 0 位,在最顶端;

当函数里面还有函数时,子函数可以访问父函数的劳动成果,可以访问父函数的[[scope]],里面还存有自己的执行期上下文(AO);

查找一个变量,scope 会从顶端开始查找作用域链,如果顶端查找不到就会查找下一个;

函数在被调用执行时,会创建一个当前函数的执行期上下文,在该执行期上下文的创建阶段,变量对象、作用域、闭包、this 指向会分别被确定,而一个 JavaScript 程序中一般来说会有多个函数,JavaScript 引擎使用调用栈来管理这些函数的调用顺序,函数调用栈的调用顺序与栈数据结构一致;

例子:

a 定义之后会产生一个 scope,scope 里面储存的是全局的执行上下文 GO,之后 a 被执行,产生了一个 AO,被放到了 scope 的最顶端,形成了一个新的作用域链,

js
function a() {
  function b() {
    var b = 234
  }
  var a = 123
  b()
}
var glob = 100
a()
function a() {
  function b() {
    var b = 234
  }
  var a = 123
  b()
}
var glob = 100
a()

image-20240704143112941

javascript 变量的生命周期

JavaScript 变量生命周期在它声明时初始化。

局部变量在函数执行完毕后销毁。

全局变量在页面关闭后销毁。

作用域链: ​

从内到外查找,先找自己局部区域有没有,再往外一层

作用域链:[[scope]]中所存储的执行期上下文(AO)对象的集合,这个集合呈链式链接,我们把这种链式链接叫作用域链;

预解析(变量提升) ​

  1. 执行代码前,会先找页面中的(var 和 function),找出来之后并把变量赋值为 undefined,函数永远是最后提升的

  2. 逐行解析:遇到赋值表达式时,会将值重新赋值给变量;

  3. 变量的提升:只会在当前的作用域中提升,提升到当前的作用域的最上面; 函数中的变量只会提前到函数的作用域最前面,不会提升到外面去;

作用域预解析必背四个步骤

  • 创建 AO/GO 对象,

  • 变量提升并赋值 undefined

  • 形参实参相统一;

  • 函数永远是最后提升的,函数最后提升到最上面;

js
// 由于函数最后提升,先提升var,赋值undefined,之后提升function,a被赋值函数,因此第一个打印函数;
// 之后a又被赋值,因此之后的值一直是a的,function已经被提升到最上面了;
console.log(a)
function a() {
  console.log('aaaa')
}
var a = 1
function a() {
  console.log('bbbb')
}
console.log(a)
// 由于函数最后提升,先提升var,赋值undefined,之后提升function,a被赋值函数,因此第一个打印函数;
// 之后a又被赋值,因此之后的值一直是a的,function已经被提升到最上面了;
console.log(a)
function a() {
  console.log('aaaa')
}
var a = 1
function a() {
  console.log('bbbb')
}
console.log(a)

this ​

this 定义 ​

  • 函数中的 this 指向,不是在定义的时候确定的,而是在调用的时候确定的;

  • 当事件放到循环里面时,是拿不到 i 的,需要用 this 代替这个,比如:

js
for(var i=0;i<oli.length;i++){
    oli[i].onmouseover = function(){
        this.style.display = "block";    //事件里面的i是无法获取到会报错的;这个this代表划过的这个;
}
for(var i=0;i<oli.length;i++){
    oli[i].onmouseover = function(){
        this.style.display = "block";    //事件里面的i是无法获取到会报错的;这个this代表划过的这个;
}
  • this 可以在 js 任何地方使用,位置不同指向不同,

  • 全局的任何地方都可以调用,window 是页面中最大的对象,

this 指向 ​

(1)触发当前事件的对象;

(2)普通函数的 this 指向 window;

(3)构造函数的 this 指向实例对象;

(4)方法中的 this 指向实例对象;

(5)原型中的 this 指向实例对象

(6)定时器中的 this 指向 window;

(7)函数作为某个对象的方法时,this 指向这个对象;

(8)函数作为某个数组的数据时,this 指向这个数组;

总结:执行代码前,会把变量的声明提前,变量值为 undefined,当遇到变量值时,并给变量赋值;

修改 this 指向 ​

js
//call和apply都可以用于修改函数中的this指向;他们的区别在于参数的传递
1、call 用于立即调用函数 并修改函数中的this指向;函数的参数按原有顺序依次传入
  	fn.call({});
2、apply 用于立即调用函数 并修改函数中的this指向;函数的参数 是apply第二个参数(数组或类数组),数组或类数组对象会自动展开成为函数的参数
  	fn.apply([]);
3、function.prorotype.bind(thisObj,arg):修改this指向为thisObj,并为函数绑定实参;返回一个新函数,需要手动执行;
    参数:thisObj:函数中this指向的对象; arg:实参;
 		fn.bind(obj,arg);
//apply和call方法在使用时,会改变this指向然后传参后立即执行函数,返回结果
//bind方法是创建了一个新的函数,在这个被创建的新的函数中,this的指向被改变,并接收了bind传递的参数。需要另外手动调用后,函数才会被执行。
//call和apply都可以用于修改函数中的this指向;他们的区别在于参数的传递
1、call 用于立即调用函数 并修改函数中的this指向;函数的参数按原有顺序依次传入
  	fn.call({});
2、apply 用于立即调用函数 并修改函数中的this指向;函数的参数 是apply第二个参数(数组或类数组),数组或类数组对象会自动展开成为函数的参数
  	fn.apply([]);
3、function.prorotype.bind(thisObj,arg):修改this指向为thisObj,并为函数绑定实参;返回一个新函数,需要手动执行;
    参数:thisObj:函数中this指向的对象; arg:实参;
 		fn.bind(obj,arg);
//apply和call方法在使用时,会改变this指向然后传参后立即执行函数,返回结果
//bind方法是创建了一个新的函数,在这个被创建的新的函数中,this的指向被改变,并接收了bind传递的参数。需要另外手动调用后,函数才会被执行。

函数返回值 ​

  1. 函数调用以后的结果,就是 return 后面的值;
  2. 如果没有 return,则返回 undefined;
  3. return 后面的代码不会再执行;
  4. return 后面有什么,最终返回结果就是什么;
  5. 直接调用函数,需要使用一个变量接收一下函数;
  6. 如果函数 return 后面没有任何值,返回结果将会是 undefined;

总结:使用 return 就必须有返回值,要么就永远不使用;

return 返回多个值时,只能返回最后一个值,如果需要返回多个值可以放到一个数组当中,或对象中返回;

返回值的类型可以是 js 中数据类型中的任何一个;

面试题:

js
var x = 1,
  y = (z = 0)
function add(n) {
  n = n + 1
}
y = add(x)
function add(n) {
  n = n + 3
}
z = add(x)
console.log(x, y, z)
var x = 1,
  y = (z = 0)
function add(n) {
  n = n + 1
}
y = add(x)
function add(n) {
  n = n + 3
}
z = add(x)
console.log(x, y, z)

解答: 两个函数都没有 return 返回值,所有 x 和 y 都为 undefined;

image-20240704143146728

函数注释 ​

image

实例:

js
/**
 *
 *@param num {Number} 数字
 *@param str {String} 名称
 *@return {Boolean} true:成年 false:未成年
 * @type { import('vite').UserConfig } // 使用ts类型
 */
function check(num, str) {
  if (num === 18) {
    return true
  }
}
/**
 *
 *@param num {Number} 数字
 *@param str {String} 名称
 *@return {Boolean} true:成年 false:未成年
 * @type { import('vite').UserConfig } // 使用ts类型
 */
function check(num, str) {
  if (num === 18) {
    return true
  }
}

函数高级 ​

缓冲运动 ​

透明度处理 ​

javascript
/* 
    作用:缓冲运动
    @para:
        eleObj:元素对象
        attr:属性
        target:目标值
*/

function bufferMove(eleObj, attr, target) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
    var cur =
      attr == 'opacity'
        ? parseFloat(getStyle(eleObj, attr)) * 100
        : parseFloat(getStyle(eleObj, attr))

    // 求步长 =   (目标值 - 当前值) /10
    var step = (target - cur) / 10

    // 小数计算精度问题
    // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
    step = step > 0 ? Math.ceil(step) : Math.floor(step)
    console.log(cur, step)

    // 判断是否到达目标值
    if (cur == target) {
      // 清除定时器
      clearInterval(eleObj.timer)
    }

    // 赋值
    // 判断
    if (attr == 'opacity') {
      // 缩小100倍设置
      eleObj.style.opacity = (cur + step) / 100
    } else {
      eleObj.style[attr] = cur + step + 'px'
    }
  }, 50)
}
/* 
    作用:缓冲运动
    @para:
        eleObj:元素对象
        attr:属性
        target:目标值
*/

function bufferMove(eleObj, attr, target) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
    var cur =
      attr == 'opacity'
        ? parseFloat(getStyle(eleObj, attr)) * 100
        : parseFloat(getStyle(eleObj, attr))

    // 求步长 =   (目标值 - 当前值) /10
    var step = (target - cur) / 10

    // 小数计算精度问题
    // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
    step = step > 0 ? Math.ceil(step) : Math.floor(step)
    console.log(cur, step)

    // 判断是否到达目标值
    if (cur == target) {
      // 清除定时器
      clearInterval(eleObj.timer)
    }

    // 赋值
    // 判断
    if (attr == 'opacity') {
      // 缩小100倍设置
      eleObj.style.opacity = (cur + step) / 100
    } else {
      eleObj.style[attr] = cur + step + 'px'
    }
  }, 50)
}

多属性到达目标值 ​

javascript
/* 
    作用:缓冲运动
    @para:
        eleObj:元素对象
        tarObj:对象  {attr:目标值,attr:目标值}
*/
function bufferMove(eleObj, tarObj) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 自定义标识   默认值是true表示所有的属性都到达目标值
    var flag = true

    for (var attr in tarObj) {
      // 获取当前属性的目标值
      var target = tarObj[attr]

      // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
      var cur =
        attr == 'opacity'
          ? parseFloat(getStyle(eleObj, attr)) * 100
          : parseFloat(getStyle(eleObj, attr))

      // 求步长 =   (目标值 - 当前值) /10
      var step = (target - cur) / 10

      // 小数计算精度问题
      // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
      step = step > 0 ? Math.ceil(step) : Math.floor(step)

      console.log(attr + ':' + cur, step)

      // 判断是否到达目标值  只要有一个属性没有到达目标值 就将flag赋值为false
      if (cur != target) {
        flag = false
      }

      // 赋值
      // 判断
      if (attr == 'opacity') {
        // 缩小100倍设置
        eleObj.style.opacity = (cur + step) / 100
      } else {
        eleObj.style[attr] = cur + step + 'px'
      }
    }

    // flag值是true就是清除定时器
    if (flag) {
      clearInterval(eleObj.timer)
    }
  }, 50)
}
/* 
    作用:缓冲运动
    @para:
        eleObj:元素对象
        tarObj:对象  {attr:目标值,attr:目标值}
*/
function bufferMove(eleObj, tarObj) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 自定义标识   默认值是true表示所有的属性都到达目标值
    var flag = true

    for (var attr in tarObj) {
      // 获取当前属性的目标值
      var target = tarObj[attr]

      // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
      var cur =
        attr == 'opacity'
          ? parseFloat(getStyle(eleObj, attr)) * 100
          : parseFloat(getStyle(eleObj, attr))

      // 求步长 =   (目标值 - 当前值) /10
      var step = (target - cur) / 10

      // 小数计算精度问题
      // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
      step = step > 0 ? Math.ceil(step) : Math.floor(step)

      console.log(attr + ':' + cur, step)

      // 判断是否到达目标值  只要有一个属性没有到达目标值 就将flag赋值为false
      if (cur != target) {
        flag = false
      }

      // 赋值
      // 判断
      if (attr == 'opacity') {
        // 缩小100倍设置
        eleObj.style.opacity = (cur + step) / 100
      } else {
        eleObj.style[attr] = cur + step + 'px'
      }
    }

    // flag值是true就是清除定时器
    if (flag) {
      clearInterval(eleObj.timer)
    }
  }, 50)
}

回调函数 ​

回调函数:将函数作为参数传递给另一个函数调用的函数(等某个动作完成后调用的函数),是解决异步操作的有效途径

  • 同步:同步代码立即执行按照代码的加载流程,同步代码是放到主栈任务队列执行
  • 异步:异步代码不是立即执行的,异步代码是放到等待任务队列 ,等到同步代码执行完毕再去执行 。
  • 目前我们接触到的异步代码:
    • 事件
    • 定时器
javascript
/* 
          作用:缓冲运动
          @para:
              eleObj:元素对象
              tarObj:对象  {attr:属性,val:目标值}
              callBack:回调函数   动画完成后调用的函数  这个参数可以省略
      */
function bufferMove(eleObj, tarObj, callBack) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 自定义标识   默认值是true表示所有的属性都到达目标值
    var flag = true

    for (var attr in tarObj) {
      // 获取当前属性的目标值
      var target = tarObj[attr]

      // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
      var cur =
        attr == 'opacity'
          ? parseFloat(getStyle(eleObj, attr)) * 100
          : parseFloat(getStyle(eleObj, attr))

      // 求步长 =   (目标值 - 当前值) /10
      var step = (target - cur) / 10

      // 小数计算精度问题
      // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
      step = step > 0 ? Math.ceil(step) : Math.floor(step)

      // 判断是否到达目标值  只要有一个属性没有到达目标值 就将flag赋值为false
      if (cur != target) {
        flag = false
      }

      // 赋值
      // 判断
      if (attr == 'opacity') {
        // 缩小100倍设置
        eleObj.style.opacity = (cur + step) / 100
      } else {
        eleObj.style[attr] = cur + step + 'px'
      }
    }

    // flag值是true就是清除定时器    所有的属性都到达目标值
    if (flag) {
      clearInterval(eleObj.timer)

      // 存在  &&  执行
      callBack && callBack()
    }
  }, 50)
}
/* 
          作用:缓冲运动
          @para:
              eleObj:元素对象
              tarObj:对象  {attr:属性,val:目标值}
              callBack:回调函数   动画完成后调用的函数  这个参数可以省略
      */
function bufferMove(eleObj, tarObj, callBack) {
  // 防止用户多次点击先清除定时器
  clearInterval(eleObj.timer)
  // 设置定时器
  eleObj.timer = setInterval(function () {
    // 自定义标识   默认值是true表示所有的属性都到达目标值
    var flag = true

    for (var attr in tarObj) {
      // 获取当前属性的目标值
      var target = tarObj[attr]

      // 获取当前值  判断  是透明度我们扩大100倍处理   因为透明度0-1之间
      var cur =
        attr == 'opacity'
          ? parseFloat(getStyle(eleObj, attr)) * 100
          : parseFloat(getStyle(eleObj, attr))

      // 求步长 =   (目标值 - 当前值) /10
      var step = (target - cur) / 10

      // 小数计算精度问题
      // 给step重新赋值  步长大于0 返回?后的值 否则返回:后的值
      step = step > 0 ? Math.ceil(step) : Math.floor(step)

      // 判断是否到达目标值  只要有一个属性没有到达目标值 就将flag赋值为false
      if (cur != target) {
        flag = false
      }

      // 赋值
      // 判断
      if (attr == 'opacity') {
        // 缩小100倍设置
        eleObj.style.opacity = (cur + step) / 100
      } else {
        eleObj.style[attr] = cur + step + 'px'
      }
    }

    // flag值是true就是清除定时器    所有的属性都到达目标值
    if (flag) {
      clearInterval(eleObj.timer)

      // 存在  &&  执行
      callBack && callBack()
    }
  }, 50)
}

闭包 ​

  • 闭包的概念:一个函数中嵌套另一个函数,另一个函数可以访问当前这个函数的局部变量,在本质上,闭包是将函数内部和函数外部连接起来的桥梁
  • 闭包作用(闭包优点和缺点):缓存数据,延长作用域链 (使用闭包需谨慎,闭包不使用的时候我们可以手动释放,例如:resFn = null)
  • 闭包的本质:形成了一个不销毁的私有作用域
  • 闭包的优点
    • 延迟变量的生命周期,如果执行上下文不被销毁,这个变量就可以一直存在
    • 形成私有作用域,使得某些变量不受外部干扰
    • 突破函数独立作用域,可以从外部拿到里面的变量
  • 缺点
    • 显而易见的是内存泄漏,因为绕过了垃圾回收机制,这个变量一直存在,过度使用闭包会导致内存占用过多
javascript
function fn() {
  var a = 2

  function test() {
    a++
    console.log(a)
  }
  return test
}

// 全局变量默认不会销毁  浏览器关闭销毁
var resFn = fn()
resFn() //3
resFn() // 4
resFn() // 5
function fn() {
  var a = 2

  function test() {
    a++
    console.log(a)
  }
  return test
}

// 全局变量默认不会销毁  浏览器关闭销毁
var resFn = fn()
resFn() //3
resFn() // 4
resFn() // 5
闭包引起的内存泄漏 ​
plain
闭包内多个函数共享词法环境,导致词法环境膨胀,从而导致无法触达也无法回收的内存空间
闭包内多个函数共享词法环境,导致词法环境膨胀,从而导致无法触达也无法回收的内存空间

递归 ​

概念:函数内部调用自己

递归可以分为两个阶段

  • 递去:递去的过程形成的私有作用域不销毁
  • 归来:在归来阶段将值返回,私有作用域销毁
javascript
//求3的阶乘   3*2*1
function sum(n) {
  // 判断 不再自己调用自己了
  if (n == 1) {
    return 1
  }
  return n * sum(n - 1) //n * 当前函数调用完成的返回值

  // 3 * sum(2);
  // 2 * sum(1)
  // 1
  // 3 *  2 * sum(1)
  // 3 * 2 *  1 => 6
}
var res = sum(3)
console.log(res)
//求3的阶乘   3*2*1
function sum(n) {
  // 判断 不再自己调用自己了
  if (n == 1) {
    return 1
  }
  return n * sum(n - 1) //n * 当前函数调用完成的返回值

  // 3 * sum(2);
  // 2 * sum(1)
  // 1
  // 3 *  2 * sum(1)
  // 3 * 2 *  1 => 6
}
var res = sum(3)
console.log(res)

5.递归快速排序

javascript
var arr1 = [12, 13, 23, 14, 20, 26, 34, 13, 16]
//  快速排序
// 思想(默认升序排列):用数组的中间项和其它项进行比较  比中间项大放到右边,比这个中间项少的放到左边 (左边和右边是一个新数组)

//arr:要处理的函数
function quickSort(arr) {
  // 数组中当中只有一项就返回
  if (arr.length <= 1) {
    return arr
  }
  // 获取中间项的索引
  var pointIndex = Math.floor(arr.length / 2)
  // 获取中间项
  var ponitVal = arr.splice(pointIndex, 1)[0]

  // 创建数组 左边的数组  和 右边的数组
  var left = [],
    right = []

  //用数组的中间项和其它项进行比较
  for (var i = 0; i < arr.length; i++) {
    var cur = arr[i]
    // 比中间项大放到右边,比这个中间项少的放到左边
    cur > ponitVal ? right.push(cur) : left.push(cur)
  }

  return quickSort(left).concat(ponitVal, quickSort(right))
}

var resArr = quickSort(arr1)

// 原数组会少一项
console.log(arr1)
console.log(resArr)
var arr1 = [12, 13, 23, 14, 20, 26, 34, 13, 16]
//  快速排序
// 思想(默认升序排列):用数组的中间项和其它项进行比较  比中间项大放到右边,比这个中间项少的放到左边 (左边和右边是一个新数组)

//arr:要处理的函数
function quickSort(arr) {
  // 数组中当中只有一项就返回
  if (arr.length <= 1) {
    return arr
  }
  // 获取中间项的索引
  var pointIndex = Math.floor(arr.length / 2)
  // 获取中间项
  var ponitVal = arr.splice(pointIndex, 1)[0]

  // 创建数组 左边的数组  和 右边的数组
  var left = [],
    right = []

  //用数组的中间项和其它项进行比较
  for (var i = 0; i < arr.length; i++) {
    var cur = arr[i]
    // 比中间项大放到右边,比这个中间项少的放到左边
    cur > ponitVal ? right.push(cur) : left.push(cur)
  }

  return quickSort(left).concat(ponitVal, quickSort(right))
}

var resArr = quickSort(arr1)

// 原数组会少一项
console.log(arr1)
console.log(resArr)

防抖和节流 ​

在开发过程中我们经常绑定高频发事件,例如 resize,scroll,mousemove,..我们可能不想让对应的事件处理函数实时触发,这个我们就要用到防抖和节流,防抖和节流都是用来做性能优化的。

防抖 ​

在一个事件发生一定时间之后,才执行特定动作(这个动作完成,一定时间后才执行一次)。

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。
  • ..
javascript
function debounce(func, wait = 500) {
  let timer = null // 通过闭包定义一个空的定时器;
  return function (...args) {
    // 必须返回一个函数,否则不成功;
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
      console.log('防抖成功') // 可以传入一个函数执行
    }, wait)
  }
} //调用方法时记得带小括号,执行返回的函数;
ipt.addEventListener('input', debounce())
function debounce(func, wait = 500) {
  let timer = null // 通过闭包定义一个空的定时器;
  return function (...args) {
    // 必须返回一个函数,否则不成功;
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
      console.log('防抖成功') // 可以传入一个函数执行
    }, wait)
  }
} //调用方法时记得带小括号,执行返回的函数;
ipt.addEventListener('input', debounce())
节流 ​

在一定时间之内,限制一个动作只执行一次 。 (过安检,30s,检查一个人,事件一直触发,隔多长时间做一次这个事件)

  • 监听滚动条位置;
  • 防止高频点击提交,防止表单重复提交;
  • ...
javascript
function throttle() {
  let flag = true // 设定一个开关
  return function () {
    if (!flag) return // 判断是否有等待延时函数,如果开关为false则return不执行;
    flag = false // 将开头为false;
    setTimeout(() => {
      console.log('节流中...')
      flag = true // 执行完定时器 开关为true;
    }, 500)
  }
}
// 第二种
export function throttle(fn, wait) {
  var pre = Date.now()
  return function () {
    var now = Date.now()
    if (now - pre >= wait) {
      fn.apply(this, arguments)
      pre = Date.now()
    }
  }
}
function throttle() {
  let flag = true // 设定一个开关
  return function () {
    if (!flag) return // 判断是否有等待延时函数,如果开关为false则return不执行;
    flag = false // 将开头为false;
    setTimeout(() => {
      console.log('节流中...')
      flag = true // 执行完定时器 开关为true;
    }, 500)
  }
}
// 第二种
export function throttle(fn, wait) {
  var pre = Date.now()
  return function () {
    var now = Date.now()
    if (now - pre >= wait) {
      fn.apply(this, arguments)
      pre = Date.now()
    }
  }
}

其他函数调用方式 ​

js
function fn(one, two, three) {
  console.log(one, two, three)
}
const a = 'hh'
const b = 'ss'
fn`this a is ${a} and b is ${b}`
function fn(one, two, three) {
  console.log(one, two, three)
}
const a = 'hh'
const b = 'ss'
fn`this a is ${a} and b is ${b}`

使用模板字符串调用函数时:

  • 第一个参数:数组,包含模板字符串分割的字符串
  • 第二个参数:模板字符串的第一个变量,后面的变量以此类推

Event Loop ​

在 JavaScript 中,异步任务被分为两种:

宏任务(MacroTask):script、setTimeout、setInterval、setImmediate、I/O、UI rendering;

微任务(MicroTask):Process.nextTick、promise、mutationObserver

这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。

Event Loop 执行顺序如下:

  • 首先执行同步代码,这属于宏任务
  • 当执行完所有同步代码后,执行栈为空,查询是否有事件队列需要执行
  • 执行所有微任务
  • 当执行完所有微任务后, 如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

image-20240705105253148

理解: javascript 按照同步执行,遇到 promise 则执行 promise 里的语句,如果有 then 则放到微任务队列中;

直到一圈执行完成,再开始执行微任务中的任务;

遇到 setTimeout 放到宏队列的最后边,直到所有的微任务执行完成,开始执行宏队列中的任务;

js
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)

async1()

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout')
}, 0)

async1()

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

柯里化函数 ​

定义:函数柯里化又叫部分求值,柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术;

理解:只传递给函数一部分参数来调用它,让它返回一个新函数去处理剩下的参数;

bind 函数也是柯里化的一种;

实现一个 currying 函数,判断当前参数长度够不够,参数不够返回一个新函数,参数够了立马执行;

js
function currying(fn, length) {
  // 第一次调用不需要指定length,主要是用来判断形参个数;
  length = length || fn.length // 保留数组长度;
  return function (...args) {
    // 返回一个新函数;并使用新函数进行传参;
    if (args.length >= length) {
      //递归出口,判断长度是否相等;
      fn.apply(this, args) // 如果满足参数,则执行;
    } else {
      // 参数未满足,重新返回新函数,进行递归;
      return currying(fn.bind(this, ...args), length - args.length)
      // 因为有传入的参数,所以使用bind将参数传入一部分;  递归函数必须返回,否则没有返回值;
    }
  }
}
function currying(fn, length) {
  // 第一次调用不需要指定length,主要是用来判断形参个数;
  length = length || fn.length // 保留数组长度;
  return function (...args) {
    // 返回一个新函数;并使用新函数进行传参;
    if (args.length >= length) {
      //递归出口,判断长度是否相等;
      fn.apply(this, args) // 如果满足参数,则执行;
    } else {
      // 参数未满足,重新返回新函数,进行递归;
      return currying(fn.bind(this, ...args), length - args.length)
      // 因为有传入的参数,所以使用bind将参数传入一部分;  递归函数必须返回,否则没有返回值;
    }
  }
}
js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function (...moreArgs) {
        return curried.apply(this, args.concat(moreArgs))
      }
    }
  }
}

// 用例
function add(a, b, c) {
  return a + b + c
}

const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 输出: 6
console.log(curriedAdd(1, 2)(3)) // 输出: 6
console.log(curriedAdd(1)(2, 3)) // 输出: 6
console.log(curriedAdd(1, 2, 3)) // 输出: 6
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function (...moreArgs) {
        return curried.apply(this, args.concat(moreArgs))
      }
    }
  }
}

// 用例
function add(a, b, c) {
  return a + b + c
}

const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 输出: 6
console.log(curriedAdd(1, 2)(3)) // 输出: 6
console.log(curriedAdd(1)(2, 3)) // 输出: 6
console.log(curriedAdd(1, 2, 3)) // 输出: 6

偏函数 ​

偏函数,partial application,也叫做部分应用、局部应用、偏应用;

偏函数指的是对一个函数应用一个或多个参数,但不是全部参数,在这个过程中创建一个新函数,这个函数用于接收剩余的参数;

偏函数和柯里函数的区别:

  • 柯里化:函数分解为多个函数,只有传入的参数数量与希望参数数量相同,函数才会调用;
  • 偏函数:没有参数数量的限制,可以使用任意数量的参数来应用它;

简单实现:传入一个函数,将偏函数化,不限制传入的数量,即可以执行;

js
function partial(fn, ...args) {
  return function (...params) {
    return fn.apply(this, [...args, ...params])
  }
}
function partial(fn, ...args) {
  return function (...params) {
    return fn.apply(this, [...args, ...params])
  }
}

惰性函数 ​

原理就是解决每次调用都要进行判断的这个问题,将函数重写,第一次运行之后就会覆写这个方法,下一次再运行的时候就不会执行判断了。

js
var foo = function () {
  var t = new Date()
  foo = function () {
    return t
  }
  return foo() // 需要返回;
}

function fn(a, b) {
  if (a == b) {
    fn = function () {}
  } else {
    fn = function () {}
  }
  return fn()
} // 如果a等于b则执行上面的函数;返回需要执行一下;

// 利用惰性函数来重写事件;
function addEvent(type, el, fn) {
  if (window.addEventListener) {
    addEvent = function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    addEvent = function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
  return addEvent()
}

// 闭包形式
var addEvent = (function () {
  if (window.addEventListener) {
    return function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    return function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
})()
var foo = function () {
  var t = new Date()
  foo = function () {
    return t
  }
  return foo() // 需要返回;
}

function fn(a, b) {
  if (a == b) {
    fn = function () {}
  } else {
    fn = function () {}
  }
  return fn()
} // 如果a等于b则执行上面的函数;返回需要执行一下;

// 利用惰性函数来重写事件;
function addEvent(type, el, fn) {
  if (window.addEventListener) {
    addEvent = function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    addEvent = function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
  return addEvent()
}

// 闭包形式
var addEvent = (function () {
  if (window.addEventListener) {
    return function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    return function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
})()