函数相关知识点总结

目标

  • 能够说出函数的多种定义和调用方式
  • 能够说出和改变函数内部this的指向
  • 能够说出严格模式的特点
  • 能够把函数作为参数和返回值传递
  • 能够说出闭包的作用
  • 能够说出递归的两个条件
  • 能够说出深拷贝和浅拷贝的区别

函数的定义

函数声明方式function

function fn() {}

函数表达式(匿名函数)

var fun = function() {}

new Function() (构造函数)

  • 这种方式比较麻烦,不符合我们平时书写习惯,但是通过这个例子我们会得出一个结论:所有的函数都是Function函数的实例对象

  • 函数也属于对象(万物皆对象)

// new Function('参数1', '参数2', '函数体')
var f = new Function('a', 'b', 'console.log(a + b)');
f(1, 2);

函数的调用

// 1. 普通函数
function fn () {
  console.log('111');
}
fn(); // 或者fn.call();

// 2. 对象的方法
var o = {
  sayHi: function() {
    console.log('222');
  }
}
o.sayHi();

// 3. 构造函数
function Star() {}
new Star();

// 4. 绑定事件函数
btn.onclick = function() {}
// 点击了事件就可以调用函数

// 5. 定时器函数
setInterval(() => {}, 1000) // 这个函数是定时器每隔一秒调用一次

// 6. 立即执行函数
(function() {
  console.log('333');
})()
// 立即执行函数是自动的调用

this指向问题

// 函数不同的调用方式决定了this指向不同
// 1. 普通函数
function fn () {
  console.log('普通函数的this' + this);
// 此时this是指向函数的调用者,即window
}
fn(); // 或者fn.call();

// 2. 对象的方法
var o = {
  sayHi: function() {
    console.log(this);
// 此时this指向的是o这个对象
  }
}
o.sayHi();

// 3. 构造函数
function Star() {
  console.log(this)
// 构造函数的this指向s这个实例对象,原型对象里面的this,指向的也是s这个实例对象
}
var s = new Star();

// 4. 绑定事件函数
btn.onclick = function() {
  console.log(this)// 这里的this指向的是函数的调用者即btn这个元素
}
// 点击了事件就可以调用函数

// 5. 定时器函数
setInterval(() => {}, 1000) // 这个函数是定时器每隔一秒调用一次
// 定时器的this指向的是window

// 6. 立即执行函数
(function() {
  console.log('333');
})()
// 立即执行函数是自动的调用
// 立即执行函数this指向的也是window

改变函数内部的this指向

Js为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部this的指向问题,常用的有bind()call()apply()三种方法

1. call()

语法: fun.call(thisArg, arg1, arg2, ...)

参数:

  • thisArg: 当前调用函数this的指向对象
  • arg1,arg2: 传递的其他参数

返回值:

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。

call()方法两个作用:一个是调用函数,简单理解为调用函数的方式,第二是可以改变函数内部的this指向.

改变函数内部this指向
  function fn(x, y) {
    console.log('111');
    console.log(this); // 正常来说这里打印的this的指向应该是window
    console.log(x + y); // 3
  }
  var obj = {
    name: 'li',
  }
  // 1. 调用函数
  fn.call();
  // 2. 改变函数内部this的指向
  fn.call(obj, 1, 2); // 通过call方法,我们可以实现把fn函数内部的this指向obj这个对象,这就是call的强大之处
call 实现继承
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

console.log(new Food('cheese', 5).name);
// expected output: "cheese"

上面的例子中,通过call改变了Product的this指向,让它指向了Food,那么Product的所有属性都被Food所继承

如果没有传递第一个参数,this 的值将会被绑定为全局对象。
var sData = 'Wisen';

function display() {
  console.log('sData value is ', this.sData);
}

display.call();  // sData value is Wisen
// 如果没有传递第一个参数,this 的值将会被绑定为全局对象,但是这是非严格模式下的情况
// 如果是严格模式下,sData的值为 undefined

call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

2. apply()
  • apply()方法调用一个函数,简单理解为调用函数的方式,但是可以改变函数的this指向

语法:fun.apply(thisArg, [argsArray])

参数:

  • thisArg:在fun函数运行时指定的this值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
  • apply的主要应用:比如说我们可以用apply借助数学内置对象求最大值

你也可以使用 arguments 对象作为 argsArray 参数。 arguments 是一个函数的局部变量。 它可以被用作被调用对象的所有未指定的参数。 这样,你在使用apply函数的时候就不需要知道被调用对象的所有参数。 你可以使用arguments来把所有的参数传递给被调用对象。 被调用对象接下来就负责处理这些参数。

改变this指向
  var person = {
      fullName: function() {
          return this.firstName + " " + this.lastName;
      }
  }
  var person1 = {
      firstName: "Bill",
      lastName: "Gates",
  }
  person.fullName.apply(person1);
获取数组的最大最小值

对于一些需要写循环以遍历数组各项的需求,我们可以用apply完成以避免循环。

const arr = [10, 2, 88, 5];

// 传统方法
let maxNum = arr[0],
    minNum = arr[0];
arr.forEach((item, i) => {
  maxNum = Math.max(maxNum, arr[i])
  minNum = Math.min(minNum, arr[i])
})

console.log(maxNum) // 88
console.log(minNum) // 2

// apply方法
const maxNum = Math.max.apply(null, arr);
const minNum = Math.min.apply(null, arr);

console.log(maxNum) // 88
console.log(minNum) // 2

apply 方法的最主要的作用是改变当前函数的 this 指向,还有一个作用就是将数组中的元素一个一个的当做参数传入方法,apply 方法和 call 方法基本相同, 此处 Math.max.apply(null, arr)Math.max.apply(null, [10, 2, 88, 5]) 相同, 所以当我们调用 apply 方法的时候,相当于把数组中的元素挨个当做参数传入方法中,这也就解决了 Math.max(min) 不能直接获取数组中的最大(小)值问题了

apply 将数组各项添加到另一个数组

我们可以使用push将元素追加到数组中。由于push接受可变数量的参数,所以也可以一次追加多个元素。

但是,如果push的参数是数组,它会将该数组作为单个元素添加,而不是将这个数组内的每个元素添加进去,因此我们最终会得到一个数组内的数组

如果不想这样呢?concat符合我们的需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组。 然而我们需要将元素追加到现有数组,这时候用apply正合适

var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements); // ["a", "b", 0, 1, 2]
3. bind()

bind()方法不能调用函数,但是也可以改变函数内部的this指向

这个方法在我们后面的开发中会经常用到

语法:fun.bind(thisArg, arg1, arg2, ...)

参数:

  • thisArg:在fun函数运行是指定的this值
  • arg1,arg2:其他参数

返回值:

返回由指定的this值和初始化参数改造的原函数的拷贝

    var o = {
      name: '小米'
    }

    function fn () {
      console.log(this);
    }

    const f = fn.bind(o); // 不会调用函数,但是可以改变原函数内部的this指向
    // 返回的是改变this指向之后产生的新函数
    f();

    // 如果有的函数我们不需要立即调用,又想改变函数内部this指向,此时用bind是最合适的
    // 比如我们有一个按钮,点击了之后就禁用按钮,三秒之后开启这个按钮
    var btn = document.querySelector('button');
    btn.onclick = function() {
      this.disabled = true;
      setTimeout(function() {
        this.disabled = false; 
        // 这里的this指向的是window,所以直接在这里这么写是不对的。
      }.bind(this), 3000)
      // 我们把定时器这个函数绑定一个bind,并且把定时器这个函数的this指向btn
    }
总结
  • callapply会调用函数,并且改变函数内部this指向
  • callapply传递的参数不一样,call传递arg1,arg2形式,apply必须是数组形式
  • bind不会调用函数,只会改变函数内部this指向
    应用场景
  • call 经常用作继承
  • apply 经常跟数组有关系,比如借助数学对象实现求数组的最大最小值。
  • bind 不调用函数,但是还想改变内部this指向,比如定时器内部改变this指向。

严格模式

  • Js除了提供正常的模式外,还提供了严格模式(strict mode)。ES5的严格模式是采用具有限制性Js变体的一种方式,即在严格模式下运行Js代码。
  • 严格模式在IE10以上才会被支持,旧版本会被忽略
  • 严格模式对正常的Js语义做了一些更改:
    1.消除了Js语法的不合理、不严谨之处
    2.消除代码运行的一些不安全之处,保证代码运行的安全
    3.提高编译器效率,增加运行速度。
    4.禁用了ECMAScript的未来版本中可能会定义的一些语法,为未来新版本的Js做好铺垫,比如一些保留字如:class,enum,extends,import,super不能做变量名
为脚本开启严格模式
<script>
    'use strict';
    // 下面的js代码会按照严格模式执行代码
</script>
<script>
    function(){
         'use strict';
    }()
</script>
为函数开启严格模式
// 为某个函数开启严格模式
function fn() {
    'use strict';
    // 只会为fn函数执行严格模式
}

function fun() {}
严格模式中的变化
  • 在正常变量中,如果变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量必须先用var声明,然后再使用
  • 严禁删除已经声明的变量,用delete 删除变量会报错。
this指向问题
  • 全局作用域下this指向不是window了,而是undefined
  • 以前构造函数不加new也是可以当做普通函数调用,在严格模式下this指向的是undefined,此时就会报错了。
  • 定时器中的this指向,以前指向的是window,在严格模式下还是window
  • 事件、对象中this指向的还是调用者。
函数变化
  • 严格模式下函数参数不允许重名。
  • 函数必须声明在顶层,新版本的Js会引入“块级作用域”(ES6中已引入),为了与新版本接轨,不允许在非函数的代码块中声明函数。

高阶函数

高阶函数是对其他函数进行操作的函数,它接收函数作为参数将函数作为返回值输出

  • 如果说函数它接收的参数是一个函数,它就是一个高阶函数
    function fn(a, b, callback) {
        console.log(a + b);
        callback && callback();
    }
    fn(1, 2, function() {
        console.log('我是最后调用的');
    })
  • 如果这个函数的返回值不是一个普通值,而是一个函数,它也是一个高阶函数

闭包

变量可以根据作用域的不同分为两种:全局变量和局部变量

  • 1.函数内部可以使用全局变量
  • 2.函数外部不可以使用局部变量
  • 3.当函数执行完毕,本作用域内的局部变量会销毁

闭包(closure)是指有权访问另一个函数作用域中变量的函数。

上面这句话抓重点,闭包首先是一个函数,其次一个作用域可以访问另外一个函数内部的局部变量这就是闭包

    function fn () {
      var num = 10;
      function fun() {
        console.log(num);
      }
      fun();
    }
    fn();
    // fun这个函数作用域,访问了另外一个函数fn里面的局部变量num
    // 说明此时fn就是一个闭包函数

    function fn () {
      var num = 10;
      function fun() {
        console.log(num);
      }
      return fun;
    }
    var f = fn();
    f();
    // 我们fn外部的作用域可以访问fn内部的局部变量
    // 类似于
    // var f = function fun() {
    //     console.log(num);
    //   }

    // 上面的代码也可以这样修改
    // 闭包就是一个典型的高阶函数
    function fn () {
      var num = 10;
      return function() {
        console.log(num);
      }
    }
    var f = fn();
    f();

闭包的主要作用:延伸了变量的作用范围

闭包案例
1.循环注册点击事件
  <ul class="nav">
    <li>榴莲</li>
    <li>芒果</li>
    <li>非鱼罐头</li>
    <li>大猪蹄子</li>
  </ul>
  <script>
    // 1. 我们可以利用动态添加属性的方式
    var lis = document.querySelector('.nav').querySelectorAll('li');
    for (let i = 0; i < lis.length; i++) {
      lis[i].index = i;
      lis[i].onclick = function() {
        console.log(this.index);
      }
    }
    // 2. 利用闭包的方式
    for (let i = 0; i < lis.length; i++) {
      // 利用for循环创建了4个立即执行函数
      // 立即执行函数也称为小闭包,因为立即执行函数里面的任何一个函数都可以使用它的i变量。
      (function(i) {
        lis[i].onclick = function () {
          console.log(i);
        }
      })(i);
    }
    // 上面这个例子显然第一种方法会更好,所以说闭包不一定是更好的(像这个例子必须要点击后才会释放内存,但是一直不点击,就会造成内存泄漏)。
  </script>
2.定时器中的闭包
  // 实现3秒之后,打印所有li元素的内容
  var lis = document.querySelector('.nav').querySelectorAll('li');
  for (let i = 0; i < lis.length; i++) {
    (function(i){
      setTimeout(function(){
        console.log(lis[i].innerHTML);
      }, 3000)
    })(i)
  }
3.计算打车价格
    // 闭包应用-计算打车价格
    // 打车起步价13(3公里内),之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格
    // 如果有拥堵情况,总价格多收取10元拥堵费
    var car = (function(){
      var start = 13;
      var total = 0;
      return {
        price: function(n) {// 正常的总价
          if(n <= 3) {
            total = start;
          } else {
            total = start + (n - 3) * 5;
          }
          return total;
        },
        yd: function(flag) {// 拥堵之后的费用
          return flag ? total + 10 : total;
        }
      }
    })()
    car.price(5);
    car.yd(true)

递归

  • 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
  • 简单说来就是函数内部自己调用自己。
  • 递归函数的作用和循环效果一样
function fn() {
    fn();
  }

  fn();
// 此时就会发生死循环,造成栈溢出的错误。

由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return

  var num = 1;
  function fn() {
    console.log('我要打印6句话');
    if(num == 6) {
      return;
    }
    num ++;
    fn();
  }

  fn();
案例一

求123...*n 阶乘

    function fn(n) {
      if(n == 1) {
        return 1;
      }
      return n * fn(n - 1);
    } 
    fn(3);
案例二

递归求斐波那契数列(1、1、2、3、5、8、13、21)

    function fn(n) {
      if(n <= 2) {
        return 1
      }
      return fn(n - 1) + fn(n - 2);
    }
    fn(6) // 8
案例三

利用递归求:根据id返回对应的数据对象

    // 我们想实现输入id号就可以返回数据对象
    var data = [{
      id: 1,
      name: '家电',
      goods: [{
        id: 11,
        gname: '冰箱',
      },{
        id: 12,
        gname: '洗衣机'
      }]
    },{
      id: 2,
      name: '服饰'
    }]
    function getId(json, id){
      var o = {};
      json.forEach(item => {
        if(item.id == id) {
          o = item;
          return item;
          // 2.我们想要得到goods的数据
        }else if(item.goods && item.goods.length > 0) {
          o = getId(item.goods, id);
        }
      });
    }
   getId(data, 1); 
   getId(data, 11); 
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容