大纲
尾调用
尾递归
数组查找指定元素模拟实现:
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')