【重学】尾递归,数组查找模拟实现

大纲
尾调用
尾递归
数组查找指定元素模拟实现:
find
findIndex
target, currentTarget,addEventListener监听函数中的this指向
函数复习
函数名,变量名,形参提升优先级

尾调用

尾调用: 函数执行的最后一个步骤,是返回另一个函数的调用,叫尾调用
优点:   
1. 尾调用,当里层函数被调用时,外层函数已经执行完,出栈了,不会造成内存泄漏
2. 在递归中,尾调用使得栈中只有一个函数在运行,不会造成性能问题


f(x) {
  return g(x)
}
// 尾调用,因为返回g(x)调用的时候,f(x)已经执行完


f(x) {
  return g(x) + 1
}
// 非尾调用,因为返回 g(x) 调用时,f(x)并未执行完,当f(x)执行完后,还有执行 g(x)+1,f(x)才执行完
// 函数只有执行完后才会出栈(执行上下文调用栈)


const a = x => x ? f() : g();
// f()和g()都是尾调用

const a = () => f() || g()
// f()非尾调用,还要接着判断

const a = () => f() && g();
// f()非尾调用

递归

递归  -- 尾递归和尾调用

1. 构成递归的条件
  - 边界条件
  - 递归前进段
  - 递归返回段
  - 当边界条件不满足时,递归前进
  - 当边界条件满足时,递归返回

2. 
Recursive:递归
factorial:阶乘

3. 尾调用和非尾调用
  - 尾调用和非尾调用的区别是 执行上下文栈不一样
  - 为调用:调用在函数结尾处
  - 尾调用的执行上下文栈,外层函数执行完就出栈,不会一层一层嵌套,不造成内存溢出
  - 尾调用自身就叫尾递归
// 尾调用
// 因为调用g(x)时,f(x)已经执行完了,就会出栈,不会压栈,不会造成内存溢出
function f(x){
    return g(x);
}
// 非尾调用
// 因为调用g(x)时,f(x)并未执行完,g(x)+1需要g(x)函数执行完,才会相加,返回后f(x)才会执行完
function f(x){
    return g(x) + 1;
}



------------------------------------------------------------------------------------

+++(例1)阶乘
     // recursive递归
    function factorial (n) {
      if (n < 2) return n
      return n * factorial(n-1)
    }
    const res = factorial(3)
    // 1. 3 => 3 * factorial(2) => 3 * 2 * factorial(1) => 3 * 2 * 1  
(分析)
    1. 每次返回一个递归的函数,都会创建一个闭包
    2. 所以维护这么多执行上下文栈,开销大,用以造成内存泄漏
    3. 优化方法:尾调用


+++(例1升级)阶乘优化
    function factorial(n, res) {
      if (n === 1) {
        return res
      }
      return factorial(n-1, n * res)
    }
(分析)
    第一次:factorial(3, 4* 1)
    第二次:factorial(2, 3* 4)
    第三次:factorial(1, 2* 12)
    第四次:24


+++(例1再升级)阶乘优化,多传了一个参数,可以用函数柯里化或者偏函数来实现

    function factorial(res, n) {
      if (n === 1) return res;
      return factorial(n * res, n-1)
    }

    function curring (fn) {
      let par_arr = Array.prototype.slice.call(arguments, 1)
      const closure = function () {
        par_arr = par_arr.concat(Array.prototype.slice.call(arguments))
        console.log(par_arr, 'par_arr')
        if (par_arr.length < fn.length) {
          return closure
        }
        return fn.apply(null, par_arr)
      }
      return closure
    }
    const curringFactorial =  curring(factorial, 1)
    const res = curringFactorial(4)
    console.log(res)

数组查找指定元素模拟实现

(1)findIndex

1. find(fn, bindThisObj)
  - 数组实列的find方法,用于找出第一个符合条件的数组成员,返回值是该成员
  - 参数:是一个回调函数
  - 过程:所有数组成员作为参数,依次执行改回到函数,直到找出第一个返回值为true的成员,并返回该成员
  - 返回值
    - 如果找到该成员,返回该成员 ( 注意:是第一个返回值为true的成员 )
    - 如果未找到,返回undefined ( 注意:未找到是返回undefined )
  - find 和 findIndex可以发现NaN,弥补了indexOf 的不足
  - find 和 findIndex都可以接收第二个参数,用来绑定回调函数的this对象
const arr = [1, 2, 3, -4, -5, 6]
function fn (value, index, arr) { // 注意:回调函数的参数有三个:分别是 value, index, arr
  return value < 0
}
const res = arr.find(fn)
console.log(res)





2. findIndex(fn, bindThisObj)
  - findIndex返回第一个满足条件的成员的位置
  - 返回值
    - 第一个满足条件的成员的位置 (注意: 是返回成员在数组中的位置 - 下标 )
    - 如果未找到,返回 -1 ------(注意:findIndex()未找到是返回 -1 ,find()未找到是返回undefined )





3. findIndex的模拟实现
  - predicate function 判定函数
const arr = [1, 2, 3, -4, -5, 6, NaN]

function findIndex(arr, predicate, context) {
  // 参数:数组,判定函数,执行上下文对象
  for(let i = 0; i < arr.length; i++) {
    if (predicate.call(context, arr[i], i, arr)) {
    // 如果判断条件成立,就返回该数组成员在数组中的位置
    // 判断函数的参数:item, index, arr
    // 判定函数所绑定的this指向:context 
      return i
    }
  }
  return -1 // 如果执行到这里,说明for循环中没有返回值,即没有满足条件的数组成员,则返回 -1
}
function predicate (item, index, arr) {
  return item  === -5
}
const res = findIndex(arr, predicate, window)
console.log(res) // 4





4. findLastIndex
  - findexLashIndex 倒序查找
function findLastIndex(arr, predicate, context) {
  // 从尾部开始循环
  for(let i = arr.length - 1; i >= 0; i--) {
  // 判断条件:最后一项的位置arr.length -1
    if (predicate.call(context, arr[i], i, arr)) {
      return  i
    }
  }
  return -1
}
function predicate(item, index, arr) {
  return item === 3
}
const res = findLastIndex(arr, predicate, window)
console.log(res)





5. 需求
  - 因为 findIndex 和 findLastIndex存在很多重复的部分
  - 可以通过传参的不同,返回不同的函数
  - 重点?如何通过参数的不同,实现正序和倒序?

const arr = [1, 2, 3, 4, - 5, 6]

function createFinderIndex (dir) { // direction 方向
  return function (arr, predicate, context) {
    const length = arr.length
    let index = dir > 0 ? 0 : length -1 // 正序还是倒序的index

    for(; index >= 0 && index < length; index += dir) {
      // 第三个条件:正序+1,倒序-1
      if (predicate.call(context, arr[index], index, arr)) {
        return index
      }
    }
    return -1
  }
}

function predicate (item, index, arr) {
  return item === 4
}

const findIndex = createFinderIndex(1) // 利用闭包,返回一个函数
const resFindIndex = findIndex(arr, predicate, window)
console.log(resFindIndex)

const findLastIndex = createFinderIndex(-1)
const resFindLastIndex = findIndex(arr, predicate, window)
console.log(resFindLastIndex)










target 和 currentTarget

1. e.currentTarget --------------------------- 正在执行的监听函数所绑定的那个节点,不会变
2. e.target ---------------------------------- 事件最初触发的节点

3. addEventListener() ------------------------ 监听函数中的this,指向事件的监听函数所绑定对象,和 currentTarget一样

函数复习

1. 函数声明
  - function 关键词
  - 变量赋值,这个匿名函数就是 函数表达式
  - const fn = function go () {};
    - 函数表达式,需要在语句结尾加上分号,表示语句结束!!!!!!!!!!!!!!!!!
    - 一般使用函数表达式都不会加具体的函数名称
    - 加了名称go主要有两个作用:
      - 可以在函数内部调用自身
      - 方便排错
      - 注意:go只在函数内部有效,在函数外部无效

2. 函数重复声明
  - 如果函数重复声明,后面的声明会覆盖前面的声明

3. 第一等公民
  - 函数就是一个可执行的值
  - 函数可以作为一种值,凡是使用值的地方,都可以使用函数
  - 因为函数与其他数据类型平等,所以又称函数为 第一等公民

4. 函数名的提升
  - js把函数名视同为变量,所以函数名也存在函数名提升
------------------
f();
var f = function (){}; // TypeError: undefined is not a function 报错

以上代码相当于:

var f = undefined
f(); // 所以会报错 f不是函数
f = function() {}
------------------

5. 不能在条件语句中声明函数
  - 因为存在函数名提升和变量名提升
  - 如 if    try 

6. 函数的name属性: 返回函数的名字

7. 函数的length属性: 返回函数(形参)的长度

8. 函数的toString()返回函数的源码

9. 函数的作用域
  - 作用域:指变量存在的范围
  - es5中存在两种作用域
    - 全局作用域:变量在整个程序中一直存在,所以地方都能读取
    - 函数作用域:变量在函数内部有效,函数外部无法访问
  - 局部变量:函数内部声明的变量是局部变量,外部无法访问
  - 全局变量:函数内部可以读取
  - 注意:对于 var 命令来说,局部变量只在函数内部有效,在其他代码块中,是全局变量,因为存在变量提升 
var x = 10
function a () {
  var x = 20
  console.log (x, 'in') // 20
}
a()
console.log (x, 'out') // 10
---------------
if (true) {
  var x = 5;
}
console.log(x);  // ------------------------------------ 5
---------------
if (false) {
  var x = 5;
}
console.log(x); // -------------------------------------- undefined,变量提升
---------------

10. 函数本身的作用域
  - 函数本身也是一个值,也自己的作用域
  - 函数的作用域是其声明时所在的作用域,与其运行时所在的作用域无关
  - 总之,函数执行时所在的作用域,就是函数定义时的作用域,与函数调用时的作用域无关
  - 函数内部声明的函数,作用域绑定在函数内部
var x = 10
const a = function () {
  console.log (x) ------------- 10,函数的作用域是函数声明时所在的作用域,与函数调用时的作用域无关
}
const b = function () {
  var x = 20
  a ()
}
b ()
----------------------
function a () {
  var x = 10
  return function () { ----------- 函数内部声明的函数,作用域绑定在函数内部
    console.log (x)
  }
}
var x = 20
const b = a ()
b () ------------------------------ 函数的作用域,是函数定义时的作用域,与函数调用时的作用域无关

11. 参数
  - length: 表示函数形参的长度
  - arguments: 函数内部的arguments对象,表示实参的具体情况
  - 函数的实参,不能省略前面的参数,会报错,如果一定要省略,要显示的传递undefined

12. 参数的传递方式
  - 传值传递:
    - 函数的参数,如果是原始类型的值,(number, string, boolean),传递方式是传值传递
    - 在函数内部修改参数,不会影响原始值
  - 传址传递
    - 函数的参数,如果是引用类型的值,(object,arrar, function ...),传递方式是传址传递
    - 在函数内部修改参数,会影响原始值,(传递的是地址)
  - 注意:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    - 如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值
-----------------------------
var p = 2;
function f(p) {
  p = 3;
}
f(p);
p // 2
相当于

var p = 2;
function f(p) { // ------ 函数参数是原始类型的值,传递方式是传值传递,函数内部修改参数,不影响原始值
  var p = 2 // --------------------------------- 局部变量,不影响函数外部
  p = 3;
}
f(p);
p // 2
-----------------------------

var obj = { p: 1 };
function f(o) { // ------ 函数的参数是引用类型的值,传递方式是传址传递,内部修改参数,会影响到原始值,修改的是地址
  o.p = 2;
}
f(obj);
obj.p // 2
-----------------------------

var obj = [1, 2, 3];
function f(o) {
  o = [2, 3, 4]; // ------ 替换掉了整个参数,而不是修改参数对象的某个属性,o指向了新地址,对源地址的值无影响
}
f(obj);
obj // [1, 2, 3]
-----------------------------

13. 同名参数
  - 如果有同名参数,取最后出现的那个值
  - 即使后面的参数没有值,或是被省略,也是以其为准
function f(a, a) {
  console.log(a);
}
f(1, 2) // 2
-----
function f(a, a) {
  console.log(a); // --------- 第二个参数实际传参时,被省略了,但是还是以其为准
}
f(1) // undefined

函数名,变量名,形参提升优先级

  • 优先级:
    函数形参 > 函数声明 > 变量声明
  • 函数名已经存在,新的覆盖旧的
    (如果函数名和已经存在的形参名,函数名相同的话,新的函数名将覆盖形参名或者覆盖旧的函数名)
  • 变量名已经存在,直接跳过变量声明
    (如果变量名和已经声明的形参名,函数名相同的话,则会直接跳过变量声明,不会影响已经存在的属性)
例一

function a (name, age) {
  console.log (name) // ------------------- name
  console.log (age) // -------------------- undefined
  var name = 'new name'
  console.log (name) // ------------------- new name
}
a ('name')


实际执行的代码如下:


function a (name, age) {
  var name = 'name'
  var age = undefined
  // var name = undefined ------------- 变量名name已经存在(形参),所以会直接跳过变量name的声明
  console.log (name) // 'name'
  console.log (age) // undefined
  name = 'new name'
  console.log(name) // 'new name'
}
a ('name')

例二

function a (name) {
  console.log (name)
  var name = '10'
  function name () {
    console.log (20)
  }
  console.log(name)
}
a ('woow-wu')


实际执行的代码如下:


function a (name) {
  var name = 'woow-wu' // ----------- 函数形参声明并赋值
  function name () { // ------------- 函数名存在,覆盖掉旧的形参
    console.log (20)
  }
  // var name = undefined // -------- 变量名存在,直接跳过变量的声明
  console.log(name) // ----------------------------------------------- 所以name 是 函数
  name = '10' // -------------------- name被重新赋值
  console.log(name) //------------------------------------------------ 所以打印是 '10'
}
a ('woow-wu')
例三

function a(name) {
    var name = 10;
    function name() {
        console.log('20')
    };
    console.log(name); // 10
}
a('wang')


实际执行的代码如下:


function a(name) {
    var name = 'wang';
    function name() { // -------------- 函数名已经存在,覆盖上面的
        console.log('20')
    };
  // var name = undefined  ---------- 变量已经存在跳过
    name = 10; // --------------------- 执行阶从新赋值
    console.log(name); // ------------- 一定要注意打印的位置是创建阶段还是执行阶段!!!!!
}
a('wang')

你可能感兴趣的:(【重学】尾递归,数组查找模拟实现)