JavaScript-函数进阶

91 阅读5分钟

1 - 函数的定义和调用

1.1 函数的定义方式

  1. 命名函数
function fn(){}
  1. 匿名函数
var fn = function(){}

所有函数都是 Function 的实例(对象) 。

1.2 函数的调用

/* 1. 命名函数 */
function fn() {
    console.log('人生的巅峰');
}
fn(); 

/* 2. 对象的方法 */
var o = {
  sayHi: function() {
    console.log('人生的巅峰');
  }
}
o.sayHi();

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

/* 4. 绑定事件函数 - 匿名函数 */
btn.onclick = function() {};   // 点击了按钮就可以调用这个函数

/* 5. 定时器函数 - 回调函数 */
setInterval(function() {}, 1000);  // 这个函数是定时器自动1秒钟调用一次

/* 6. 立即执行函数 */
(function() {
	console.log('人生的巅峰');
})();

2 - this指向

2.1 函数内部的this指向

这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this 的指向不同,一般指向我们的调用者。

2.2 改变函数内部 this 指向

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

① call方法:经常做属性的继承

call()方法调用函数的方式,它可以改变函数的 this 指向。

应用场景:经常做属性的继承

var o = {
	name: 'andy'
}
function fn(a, b) {
     console.log(this);
     console.log(a+b)
};
fn(1,2) // 此时的this指向的是window 运行结果为3
fn.call(o,1,2) //此时的this指向的是对象o,参数使用逗号隔开,运行结果为3

以上代码运行结果为:

② apply方法:参数使用数组传递,经常跟数组有关系

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

应用场景:参数使用数组传递,经常跟数组有关系

var o = {
	name: 'andy'
}
function fn(a, b) {
     console.log(this);
     console.log(a+b)
};
fn() // 此时的this指向的是window 运行结果为3
fn.apply(o,[1,2]) //此时的this指向的是对象o,参数使用数组传递 运行结果为3

//应用举例:求数组中的最大值a
var arr = [1, 66, 3, 99, 4];
var max = Math.max.apply(Math, arr); //这样就不用一个一个遍历了,直接使用数学方法就可以了
console.log(max);  // 99

③ bind方法:不调用函数,但是还想改变this指向

bind() 方法不会调用函数,但是能改变函数内部 this 指向,返回的是原函数改变this之后产生的新函数拷贝。

如果只是想改变 this 指向,并且不想调用这个函数的时候,可以使用bind。

应用场景:不调用函数,但是还想改变this指向

var o = {
  name: 'andy'
};

function fn(a, b) {
	console.log(this);
	console.log(a + b);
};
var f = fn.bind(o, 1, 2); //此处的f是bind返回的新函数
f(); //调用新函数  this指向的是对象o 参数使用逗号隔开

//bind方法应用场景:我们想让按钮点击之后不可用,3s之后自动变为可用
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = function() {
        this.disabled = true;
        setTimeout(function() {
            this.disabled = false; //现在的this就是指向按钮
        }.bind(this), 3000); //本来定时器的this指向window,这里让this指向按钮
    }
}

④ call、apply、bind三者的异同

  • 共同点 : 都可以改变this指向

  • 不同点:

    • call 和 apply 会调用函数,并且改变函数内部this指向。
    • call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递。
    • bind 不会调用函数,可以改变函数内部this指向。
  • 应用场景

    1. call 经常做属性继承。
    2. apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值。
    3. bind 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向。

3 - 严格模式

3.1 什么是严格模式

JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript变体的一种方式,即在严格的条件下运行 JS 代码。严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。

严格模式对正常的 JavaScript 语义做了一些更改:

  1. 消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
  2. 消除代码运行的一些不安全之处,保证代码运行的安全。
  3. 提高编译器效率,增加运行速度。
  4. 禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class,enum,export, extends, import, super 不能做变量名

3.2 开启严格模式

严格模式可以应用到整个脚本或个别函数中,因此在使用时,我们可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。

  1. 为脚本开启严格模式

脚本中,有的代码是严格模式,有的代码不是严格模式,我们可以将严格模式的代码放在一个立即执行的匿名函数之中,这样独立创建一个作用域而不影响其他代码。

<script> 
  (function (){
    // 在当前的这个自调用函数中有开启严格模式,当前函数之外还是普通模式
    'use strict';
    var num = 10;
    function fn() {}
  })();
</script>

//或者
<script>
 'use strict';  // 当前script标签开启了严格模式
  // 因为"use strict"加了引号,所以老版本的浏览器会把它当作一行普通字符串忽略掉
</script>

<script>
  // 当前script标签未开启严格模式
</script>
  1. 为函数开启严格模式

要给某个函数开启严格模式,需要把“use strict”; (或 'use strict'; ) 声明放在函数体所有语句之前。

function fn() {
  'use strict'; // 当前fn函数开启了严格模式
  return "123";
} 

3.3 严格模式中的变化

严格模式对 Javascript 的语法和行为,都做了一些改变。

  1. 必须先声明变量,再使用变量

在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用 var 命令声明,然后再使用。

'use strict'
num = 10     
console.log(num) // 报错:ReferenceError: num is not defined
  1. 严禁删除已经声明变量
'use strict'
var num2 = 1;
delete num2; // 报错:Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
  1. this 指向问题

以前在全局作用域函数中的 this 指向 window 对象。严格模式下全局作用域中函数中的 this 指向 undefined。

'use strict'
function fn() {
 console.log(this); // undefined
}
fn();  

以前构造函数不加 new 也可以调用,当普通函数,this 指向 window 对象。严格模式下,构造函数必须加 new 调用,如果构造函数不加new调用,this 指向的是undefined,如果给他赋值则会报错。

'use strict'
function Star() {
	 this.sex = '男'; // this 指向 undefined
   // 报错:Uncaught TypeError: Cannot set property 'sex' of undefined
} 
var ldh = Star();
  • new 实例化的构造函数中的this还是指向创建的对象实例。
  • 定时器 this 还是指向 window 。
  • 事件、对象还是指向调用者。
  1. 函数变化

非严格模式下函数允许有重名的参数,严格模式下函数不能有重名的参数

'use strict'
function fn(x, x, z) {
 console.log(x,x,z); // undefined
}
fn(1,2,3); // 报错:Uncaught SyntaxError: Duplicate parameter name not allowed in this context

非严格模式下函数可以声明在所有地方,严格模式下函数必须声明在顶层。新版本的 JavaScript 会引入“块级作用域”( ES6 中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。

'use strict'
if (true) {
  function f() {} // !!! 语法错误
  f();
}

for (var i = 0; i < 5; i++) {
  function f2() { // !!! 语法错误
  }
  f2();
}

function baz() { // 合法
  function eit() { } // 同样合法
}

更多严格模式要求参考

4 - 高阶函数

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

function fn(callback) {
  callback && callback();// 调用回调函数
}
fn(function() { alert('hi') }) // 函数当做参数(回调函数)

function fn(){
  return function() {}    // 函数当做返回值
}
fn();

此时 fn 就是一个高阶函数。函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数,同理函数也可以作为返回值传递回来。

5 - 闭包

5.1 变量的作用域复习

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

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

5.2 什么是闭包

能够访问另外一个函数作用域的变量的函数就是闭包

函数嵌套函数,内部函数可以引用外部函数的参数和变量,这时候内部函数就是个闭包。

function fn1() {
   var num = 10;
   function fn2(){
     console.log(num); //fn2访问了fn1中的num,这时候fn2就是闭包
   }
   fn2()
}
fn1();

5.3 闭包的好处

可以使变量长期驻扎在内存中(一般函数执行完毕,变量和参数会被销毁)

function aaa() {
  var a = 1; // a会一直在内存中
  return function() {
    a++;
    alert(a);
  }
}

var bbb = aaa();
bbb(); // 2
bbb(); // 3
bbb(); // 4

这样我们就做到了,全局作用域下访问局部变量(a)的效果。

5.4 闭包的案例

① 点击 li 输出当前 li 的索引号

如果这样写:

// nav 里面有 5 个 li
var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
    lis[i].onclick = function() {
         console.log(i);
    }
}

由于for循环是同步的,立马执行,onclick是异步的,点击之后才会执行,所以上面代码运行之后,无论点击哪一个li,打印都是4。在这里整个for循环是一个作用域,当for循环执行完毕之后,每一个li都被注册了一次点击事件,这时候i就是4,所以无法区分是哪个li,所以console.log(i) 的时候打印的都是4。

以前我们是利用动态添加属性的方式,给每个 li 添加 index,如下:点击每个小 li 分别打印 0 1 2 3。

var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
    lis[i].index = i;
    lis[i].onclick = function() {
        // console.log(i);
        console.log(this.index);
    }
}

回忆:在 OC 中我们使用 for 循环创建按钮,点击按钮打印按钮的索引,按钮的索引也是通过 tag 值绑定的,和上面的index方式是一样的。

学完闭包,我们可以利用闭包的方式得到当前 li 的索引号。

var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
(function(i) {
    lis[i].onclick = function() { // 内部函数用到了外部函数的参数i的值,所以这个内部函数是个闭包
      console.log(i);
    }
 })(i); // 立即执行函数,传入i的值
}

这时候每个 li 的 i 值都在内存中,所以点击每个 li 分别打印 0 1 2 3。

② 3秒之后,打印所有 li 元素的内容

var lis = document.querySelector('.nav').querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
   (function(i) {
     setTimeout(function() {
     console.log(lis[i].innerHTML); // 内部函数用到了外部函数的参数i的值,所以这个内部函数是个闭包
     }, 3000)
   })(i); // 立即执行函数,传入i的值
}

③ 计算打车价格

打车起步价13(3公里内),之后每多一公里增加 5块钱,用户输入公里数就可以计算打车价格,如果有拥堵情况,总价格多收取10块钱拥堵费。

var car = (function() {
     var start = 13; // 起步价  局部变量
     var total = 0;  // 总价  局部变量
     return {
       // 正常的总价
       price: function(n) { // 内部函数用到了外部函数的变量start和total,所以这个内部函数是个闭包
         if (n <= 3) {
           total = start;
         } else {
           total = start + (n - 3) * 5
         }
         return total;
       },
       // 拥堵之后的费用
       yd: function(flag) { // 内部函数用到了外部函数的变量start和total,所以这个内部函数是个闭包
         return flag ? total + 10 : total;
       }
	}
 })(); // 立即执行函数
console.log(car.price(5)); // 正常价格:23
console.log(car.yd(true)); // 拥堵:33

④ 闭包思考题

下面代码有没有产生闭包?

var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function() {
      console.log(this) // 方法内部的this指向方法调用者object
      return function() {
          console.log(this) // 普通方法内部的this指向Window
          return this.name;
      };
  }
};
console.log(object.getNameFunc()())
// 打印:object window "The Window"

// 此时没有闭包的产生,因为没有局部变量
var name = "The Window";  
var object = {    
    name: "My Object",
    getNameFunc: function() {
        var that = this; // 这里的that指向object,是个局部变量
        return function() { // 内部函数引用了外部函数的 that 变量,所以这个内部函数是个闭包
            return that.name;
        };
    }
};
console.log(object.getNameFunc()())
// 打印:"My Object"

// 此时有闭包的产生,引用 that 的内部函数就是闭包

6 - 递归

6.1 什么是递归

递归函数: 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。简单理解:函数内部自己调用自己,这个函数就是递归函数。

注意: 递归函数的作用和循环效果一样,由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件return。

6.2 递归的使用

① 利用递归求1~n的阶乘

//利用递归函数求1~n的阶乘 1 * 2 * 3 * ..n
 function fn(n) {
     if (n == 1) {
       return 1; //结束条件
     }
     return n * fn(n - 1);
 }
 console.log(fn(3)); // 3 * 2 * 1 = 6

② 利用递归求斐波那契数列

// 斐波那契数列:1、1、2、3、5、8、13、21...

//用户输入一个数字 n 就可以求出斐波那契数列对应序列的值
function fb(n) {
  if (n === 1 || n === 2) { // 第一项第二项为1
        return 1;
  }
  // 其他项为前两项之和
  return fb(n - 1) + fb(n - 2);
}
console.log(fb(3)); // 2

③ 利用递归遍历数据

// 我们想要输入id号,就可以返回数据对象
var data = [{
    id: 1,
    name: '家电',
    goods: [{
        id: 11,
        gname: '冰箱',
        goods: [{
                id: 111,
                gname: '海尔'
            }, {
                id: 112,
                gname: '美的'
            }]
      }, {
        id: 12,
        gname: '洗衣机'
      }]
  }, {
    id: 2,
    name: '服饰'
  }];

//1.利用 forEach 去遍历里面的每一个对象
 function getID(data, id) {
   var o = {}; //空对象
   data.forEach(function(item) {
     // console.log(item); // 2个数组元素
     if (item.id == id) {
       o = item; 
       return o; // 如果找到id,就返回这个对象
       // 里面应该有goods这个数组并且数组的长度不为 0 
     } else if (item.goods && item.goods.length > 0) {
       // 2. 我们想要得里层的数据 11 12 可以利用递归函数
       o = getID(item.goods, id);
     }
   });
   return o; // 最后返回
}
console.log(getID(data, 111)); // 找到了海尔

7 - 浅拷贝和深拷贝

回忆:在 OC 中,拷贝的目的就是产生一个独立的对象,不影响原来的对象。对于 NSString、NSArray、NSDictionary等,由于只有一层,所以浅拷贝就是指针拷贝,深拷贝是整个复制一份,如果有对象嵌套对象的对象,那么就要实现对象的 copy 方法。具体是深拷贝还是浅拷贝由系统决定,只要达到产生一个独立的对象的目的(并且节省内存)就行了。

JS 中的深拷贝浅拷贝就是对象嵌套对象的这种情况。

7.1 浅拷贝

JS 中的浅拷贝有三种方式:

  1. 使用=直接赋值,只是拷贝指针,所以是浅拷贝
  2. 使用 for 循环只赋值外层,内层还是拷贝指针,所以也是浅拷贝
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    }
};
var o = {};
for (var k in obj) {
    // k 是属性名  obj[k] 属性值
    o[k] = obj[k];
}
//现在就把obj拷贝给o了,只不过对于更深层次的拷贝是指针拷贝,比如:obj里的msg和o里的msg指向的是同一个对象

o.msg.age = 20;   //修改o里面的age为20
console.log(obj); //obj里面的age也变成了20
  1. ES6中 assign() 就可以实现浅拷贝,和上面效果一样
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    }
};
var o = Object.assign(obj); //将obj浅拷贝给o

o.msg.age = 20;   //修改o里面的age为20
console.log(obj); //obj里面的age也变成了20

7.2 深拷贝

采用递归去拷贝所有层级。

// 深拷贝拷贝多层, 每一级别的数据都会拷贝.
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    },
    color: ['pink', 'red']
};
var o = {};

// 封装函数 
function deepCopy(newobj, oldobj) {
    for (var k in oldobj) {
        // 判断我们的属性值属于那种数据类型
        // 1. 获取属性值  oldobj[k]
        var item = oldobj[k];
        // 2. 判断这个值是否是数组
        if (item instanceof Array) { //数组也属于对象,所以先判断数组
            newobj[k] = [];
            deepCopy(newobj[k], item)
        } else if (item instanceof Object) {
            // 3. 判断这个值是否是对象
            newobj[k] = {};
            deepCopy(newobj[k], item)
        } else {
            // 4. 属于简单数据类型
            newobj[k] = item;
        }
    }
}

deepCopy(o, obj);
console.log(o);
o.msg.age = 20;   //修改o的age为20
console.log(obj); //不影响obj的age值
var arr = [];
console.log(arr instanceof Array); //true
console.log(arr instanceof Object); //true