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的执行环境从栈中弹出。
尾调用优化的条件
- 代码在严格模式下执行;
- 外部函数的返回值是对尾调用函数的调用
- 尾调用 函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
闭包
闭包指的是那些引用了另一个函数作用域变量的函数,通常在嵌套函数中实现。
使用闭包可以在函数外部操作函数内部的变量,还可以延长变量的声明周期。
闭包会保留他们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能会导致内存过度占用。
立即执行函数
立即执行函数(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中,为啥说函数是第一类对象
函数可以作为参数传递给其他函数,作为其他函数的值返回,分配给变量,也可以存储在数据结构中。
函数也是对象,可以有属性,可以赋值给一个变量,总的开始对象能做的他能做,普通对象不能做的她也能做。
函数声明与函数表达式的区别
-
函数声明:使用function声明的函数
foo(); // 在函数声明之后调用 foo,可以正常调用。因为 foo 被提前到最前面定义了。 function foo() { return true; } -
函数表达式:使用var + function定义
foo(); // 在函数表达式之前调用函数,报错。因为这时候还没有 foo 这个变量。 var foo = function() { return foo; };解析器对函数声明和函数表达式是不一样的,解析器首先读取函数声明进行函数提升,可以在函数声明之前调用。但是函数表达式不可以,使用var定义的只提取定义并未赋值。
对this对象的理解
this是执行上下文中的一个属性,他指向最后一次调用这个方法的对象。
- 函数调用模式:当函数不是一个对象的属性时,直接做为函数来调用时,this指向全局对象
- 方法调用模式:如果一个函数做为一个对象的方法来调用时。this指向这个方法
- 构造函数调用模式:如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。
- apply,call,bind调用模式:可以显示的指定调用函数的this指向。
- 箭头函数:this指向父作用域的this。
对闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数(闭包是函数中的一个属性)。创建闭包最常用的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途:
-
使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法创建私有变量。
-
使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个对象的引用,所以这个变量不会被回收。
使用闭包解决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中本身并没有函数重载这一特性,但是可以通过判断函数个数来实现函数的重载。
-
使用函数内类对象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]); } } -
通过闭包 + 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 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
- 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作。