js之函数相关知识

121 阅读10分钟

js中的函数时一个对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而不一定与函数本身紧密绑定。

箭头函数

ES6新增了使用箭头语法定义函数表达式的能力。

let sum = (a,b)=>{
    return a + b;
}
console.log(sum(1,2)) // 3;

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用arguments、super和new.target,也不能用作构造函数,箭头函数也没有prototype属性。

函数声明和函数表达式

js引擎在任何代码执行之前,会先读取函数声明,并执行上下文中生成函数定义。

而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

//函数声明
console.log(sum(1,2));//3
function sum(a,b){
	return a + b;
}

//函数表达式
console.log(sum(1,2));//会报错
var sum = function(a,b){
	return a + b;
}

原因:使用函数声明,代码在执行之前会进行函数声明提升,会将函数的声明和定义提前。而使用函数表达式,js会将var定义的变量声明提升,并赋值为undefiend。

函数内部属性

arguments

arguments是一个类数组对象,包含调用函数时传入的所有参数,这个对象只有在以function关键字定义函数时才会有。

arguments对象中还有一个属性callee属性,是一个指向arguments对象所在函数的指针。

function f(num){
    if(num <= 1) return 1;
    return arguments.callee(num - 1); // 使用arguments.callee 代替 f。当函数名f修改时,递归调用的部分不用修改。
}

this

在标准函数中和箭头函数中有不同的行为

在标准函数中,this引用的是把函数当成方法的调用的上下文对象。

window.color = 'red'
let obj = {
    color:'green';
}
function say(){
    console.log(this.color);
}

say(); // red
obj.say = say;
obj.say() // green

在箭头函数中,this引用的是定义箭头函数的上下文。

window.color = 'red'
let obj = {
    color:'green';
}
let say = ()=>{console.log(this.color);}

say(); // red
obj.say = say;
obj.say() // red

caller

caller是函数对象上的一个属性,这个属性引用的是调用当前函数的函数,如果是在全局作用域中调用的则为null。

function outer(){
    inner();
}
function inner(){
    console.log(inner.caller);
}
outer(); // outrer()

new.target

js中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。

new.target是es6新增加的属性,用于检测函数是否使用new关键字调用的new.target属性。如果函数正常使用,则返回undefined。如果使用new关键字调用,则返回被调用的构造函数。利用此属性,可以创建抽象类,禁止实例化。

function Person(){
    if(new.target == Person){
        throw "该类为抽象类,不允许实例化"
    }
}

函数的属性和方法

属性

每个函数都有两个属性:length和prototype;

length:保存函数定义的命名参数的个数

prototype:保存引用类型所有实例方法的地方。

方法

apply和call:都会以指定的this值来调用函数,会设置调用函数时函数体内部this对象的值。

apply:接收两个参数:函数内this的值和一个参数列表。

call:接收多个参数,第一个时函数内this的值,接下来就是被调用函数的参数逐个传递。

apply和call的作用一样,知识传递参数的形式不同。

window.a = 1;
function sum(b,c){
	return this.a + b + c;
}

console.log(sum(1,1)); // 3;
let obj = {
    a:2;
}
console.log(sum.call(obj,1,1)) // 4
console.log(sum.apply(obj,[1,1])) // 4

尾调用优化

尾调用

外部函数的返回值时一个内部函数的返回值

function outer(){
	return inner();
}

如果没有尾调用优化:当执行到return语句的inner()函数时,会将inner执行的环境推到栈中。

有尾调用优化:当执行到return语句的inner函数时,引擎发现一个栈弹出也没问题,因为inner的返回值就是outer的返回值,所以在inner的执行环境进栈之前,会将outer的执行环境从栈中弹出。

尾调用优化的条件

  1. 代码在严格模式下执行;
  2. 外部函数的返回值是对尾调用函数的调用
  3. 尾调用 函数返回后不需要执行额外的逻辑
  4. 尾调用函数不是引用外部函数作用域中自由变量的闭包

闭包

闭包指的是那些引用了另一个函数作用域变量的函数,通常在嵌套函数中实现。

使用闭包可以在函数外部操作函数内部的变量,还可以延长变量的声明周期。

闭包会保留他们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能会导致内存过度占用。

立即执行函数

立即执行函数(IIFE)类似于函数声明,但由于被包含在括号中,所以会被解释成函数表达式;

(function(){
    var i = 1;
})()
console.log(i);//报错。

立即执行函数主要用于为了防止变量定义外泄,函数执行完毕之后,其作用域链就可以被摧毁。在ES6之后IIFE就没有那么必要了,因为块级作用域中的变量就可以实现变量隔离。

私有变量

js中没有私有成员的概念,所有对象属性都是公有的。不过具有私有变量的概念,任何定义在函数或块中的变量,都可以认为是私有的,因为在外部无法访问其中的变量。

在函数内定义的变量只能在函数内使用,不能再函数外部访问,但是如果这个函数中创建了闭包,则这个闭包能通过其作用域链访问到其外部的变量。所有基于闭包能够创建出能够访问私有变量的公有方法。

function Person(name){
    this.setName = function(val){
        name = val;
    }
    this.getName = function(){
        return name;
    }
}
(function(){
 	let name = "";
    Person = function(val){
        name = val;
    }
    
    Person.prototype.getName = function(){
        return name;
    }
    Person.prototype.setName = function(val){
        name = val;
    }
})()
let per = new Person("atuotuo");
console.log(per.getName()) // atuotuo
per.setName('tuotuo');
console.log(per.getName()) // tuotuo

常见面试题

在JavaScript中,为啥说函数是第一类对象

函数可以作为参数传递给其他函数,作为其他函数的值返回,分配给变量,也可以存储在数据结构中。

函数也是对象,可以有属性,可以赋值给一个变量,总的开始对象能做的他能做,普通对象不能做的她也能做。

函数声明与函数表达式的区别

  1. 函数声明:使用function声明的函数

    foo(); // 在函数声明之后调用 foo,可以正常调用。因为 foo 被提前到最前面定义了。
    function foo() {
       return true;
    }
    
  2. 函数表达式:使用var + function定义

    foo(); // 在函数表达式之前调用函数,报错。因为这时候还没有 foo 这个变量。
    var foo = function() {
       return foo;
    };
    

    解析器对函数声明和函数表达式是不一样的,解析器首先读取函数声明进行函数提升,可以在函数声明之前调用。但是函数表达式不可以,使用var定义的只提取定义并未赋值。

对this对象的理解

this是执行上下文中的一个属性,他指向最后一次调用这个方法的对象。

  1. 函数调用模式:当函数不是一个对象的属性时,直接做为函数来调用时,this指向全局对象
  2. 方法调用模式:如果一个函数做为一个对象的方法来调用时。this指向这个方法
  3. 构造函数调用模式:如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。
  4. apply,call,bind调用模式:可以显示的指定调用函数的this指向。
  5. 箭头函数:this指向父作用域的this。

对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数(闭包是函数中的一个属性)。创建闭包最常用的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途:

  1. 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法创建私有变量。

  2. 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个对象的引用,所以这个变量不会被回收。

使用闭包解决var定义的变量问题

for(var i = 0;i < 5;i++){
    setTimeout(()=>{
        console.log(i);
    },1000 * i);
} // 会输出5 5 5 5 5

// 解决1:使用闭包
for(var i = 0;i < 5;i++){
   	(function(j){
        setTimeout(()=>{
        	console.log(j);
    	},1000 * j);
    })(i);
} // 会输出 0 1 2 3 4

实现call、apply以及bind函数

Function.prototype.call(context){
    if(typeof this !== 'function') return;
    
    let arg = [...arguments].slice(1);
    
    context = context || window;
    context.fn = this;
    
    let res = context.fn(...arg);
    delete context.fn;
    return res;
}

Function.prototype.apply(context){
    if(typeof this !== 'function') return;
    
    context = context || window;
    context.fn = this;
    
    let arg = arguments[1];
    let res;
    if(arg) res = context.fn(...arg);
    else res = context.fn();
    
    delete context.fn;
    return res;
}

Function.prototype.bind(context){
    if(typeof this !== 'function') return;
    let arg = [...arguments].slice(1);
    fn = this;
    
    return function Fn(){
        return fn.apply(
        	this instanceof Fn?this:context,
            arg.concat([...arguments]);
        )
    }
}

函数重载

函数重载的定义:函数名相同,参数类型、顺序、个数不同。

JS中本身并没有函数重载这一特性,但是可以通过判断函数个数来实现函数的重载。

  1. 使用函数内类对象arguments来判断参数个数和参数类型

    function fn1(name){
    	console.log(name);
    }
    
    function fn2(name,age){
    	console.log(name,age);
    }
    // 定义一个工厂函数
    function fn()
    {
        switch(arguments.length){
            case 1:
                fn1(arguments[0]);
                break;
            case 2:
                fn2(arguments[0],arguments[1]);
        }
    }
    
  2. 通过闭包 + arguments + length方式实现

    function HeavyLoad(obj,name,fn){
        // 1.首先获取obj对象中名为name的方法
        let old = obj[name];
        // 2.定义新方法
        obj[name] = function(){
            if(fn.length === arguments.length){
                return fn.apply(this,arguments);
            }else{
                return old.apply(this,arguments);
            }
        }
    }
    HeavyLoad(window,'fn',function(name){
        console.log(name);
    })
    
    HeavyLoad(window,'fn',function(name,age){
        console.log(name,age);
    })
    fn('atuotuo') // atuotuo
    fn('atuotuo',18)//atuotuo 18
    

函数柯里化

函数柯里化是高阶函数的一种特殊应用

函数柯里化:把一个接收多个参数的函数转变成接收一个单个参数的函数,并且返回一个函数。

防抖函数

防抖:把多次执行组合成一次执行,比如设置一个时间间隔是5秒,当时间触发的事件超过5秒,回调函数才会执行,如果5秒内,事件又被触发,则刷新这个5秒。

只执行最后一个被触发的,清除之前的异步任务。

应用:输入框输入联想,表单提交

function debounce(action,context,delay){
    let timer = null;
    return function(){
        clearTimeout(timer);
        timer = null;
        timer = setTimeout(()=>{
            action.apply(context);
        },delay)
    }
}

节流函数

只在开始执行一次,未执行完成过程中触发的忽略,核心在于开关锁。

应用:页面滚动事件处理

function throttle(action,context,delay){
    let timer = null;
    return function(){
        if(timer) return true;
        
        action.apply(context);
        timer = setTimeout(()=>{
            clearTimeout(timer);
            timer = null;
        },delay)
    }
}

简单说说对函数式编程的理解,以及优缺点

主要的编程范式有三种:命令式编程,声明式编程和函数式编程

相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程。

优点

  • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
  • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
  • 隐性好处。减少代码量,提高维护性

缺点

  • 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
  • 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作。