• 有问题请联系QQ:2374146085
  • 有问题请联系QQ:2374146085

JS进阶-函数

2年前 (2021-10-03) 1544次浏览 已收录 3个评论 扫描二维码

函数定义

其中bind、柯里化、this、箭头函数、call、原型链以及继承晦涩难懂,还需要仔细复习文章内容。

匿名函数

var fn=function(){
    return 1
}
var fn2=fn
fn.name //name等于fn
fn2.name //name等于fn

具名函数

function fn3(){
    return 3
}
var fn5=function fn4(){
//此时fn4的作用域只在大括号内部,外部的访问不到
}
console.log(fn3)//fn3可以访问到

箭头函数

//接收一个e返回一个e+1
var fn6=e=>e+1
var fn6=(e,f)=>{console.log(1); return e+f}

this

this对象就是(等价于)call的第一个参数

function f(){
    console.log(this)
    console.log(arguments)
}
//call的第一个参数永远是this对象(没有参数可以写undefined),后面所有的参数会变成数组
f.call()           //不传默认this指的window了 arguments指的[]
f.call({name:'zzl'})         //此时this指的{name:'zzl'}  arguments指的[]
f.call({name:'zzl'},1)    //this指的{name:'zzl'}   arguments指的[1]
f.call({name:'zzl'},1,2)    //this指的{name:'zzl'}   arguments指的[1,2]
f()是阉割版的call,没有办法指定thsi,比如

function f(){
    console.log(this)
    console.log(arguments)
}
f(1)
//输出window   浏览器默认是window,没有办法指定
//接着输出[1]

加深理解this

var person={
    name:'zzl', //下面的sayHi没有拿到
    sayHi:function(person){ //sayHi和function函数没有任何关系,只是sayHi存了这个函数的地址,
                            //所以这个function函数是独立于person这个函数存在的
        console.log('i am '+person.name)
    }
}
//没有this的写法
person.sayHi(person) //这样必须传一个对象(自己),不传就接受不到任何信息

person.sayHi()

//////////////////////////////////////////////////////

var person={
    name:'zzl', //下面的sayHi会拿到
    sayHi:function(){
        console.log('i am '+this.name) //用传过来的对象的name
    }
}

//因为这是以person为this,所以此时上面的this对象就是zzl,即以person为this调用的sayHi
person.sayHi() 
//等价于下面
//因为this对象就是call的第一个参数,即以person为this调用的sayHi
person.sayHi.call(person) //推荐使用

// fn() 等价于 fn.call()

三段论

  • 参数的值只有在传参的时候才能确定
  • 然后,this又是第一个参数
  • 所以this的值只有在传参的时候才能确定

面试题

function a(){
       console.log(this)
}

根据三段论,故答案是不存在。

但是当调用的时候

a()   //在浏览器上的时候,上面的this就是window了,即全局变量

但是在函数里声明浏览器不要给代码乱添加默认的值

function a(){ 
          'use strivt'
          console.log(this)
 }
a() //此时的this就是undefined了

如果使用对象this又等于?

function a(){
    'use strict'
    console.log(this)
}
var obj={
    sayThis:a
}
obj.sayThis() //this是obj,因为以obj为this调用的sayThis

apply

function fum(){
    var n=0;
    for(var i=0;i<arguments.length;i++){
        n+=arguments[i]
    }
    return n
}
var a = [1,2,3,4,5,6]
sum.apply(undefined,a)
//sum.call(undefined,a[0],a[1],a[2]....

call与apply的区别

  • fn.call(asThis,p1,p2)是函数的正常调用方式
  • 当不确定参数的个数时,就使用apply
  • fn.apply(asThis,params)

bind

call和apply是直接调用函数,同时指定this和arguments。而bind则是返回(输出)一个新函数(并没有调用原来的函数),这个新函数会call原来的函数(也改变了fn的this指向),call的参数就是传给bind的参数

黑马老师讲解的

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

fun.bind(thisarg,arg1,arg2...)
  • thisarg 在fun函数运行指定的this值
  • arg1,arg2 :传递的其他参数
  • 返回由指定的this值和初始化参数改造的原函数拷贝
  • 它的返回值就是fun这个函数改造完的的函数(原函数的拷贝),产生一个新的函数,返回给我们
  • 如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向,此时就用bind
var o={
    name:'zzl'
};
function fn(a,b){
    console.log(this);
    console.log(a+b);
};
// fn.bind(o);   fn绑定了bind()方法,
// 它不会调用fn这个原来的函数,它只是改变原来函数内部的this指向,即指向了o,
// 相当于产生一个原函数改变this之后新函数,需要一个接住这个新的函数

var f=fn.bind(o); //this指向了o
f(); // 结果{name:'zzl'}
var f=fn.bind(o,1,2);
f(); // 结果{name:'zzl'} 3

禁用3秒按钮应用

<script>
var btn=document.querySelector('button');
btn.onclick=function(){
    this.disabled=true; //这个this指向的是btn这个按钮
    setTimeout(function(){
        // this.disabled=false 定时器函数里面的this指向的是window,并不能禁用btn按钮
        this.disabled=false;//定时器函数里面的this指向的是window 
    //由于定时3s,所以这个定时器不会立马执行,
    // 但是会改变原函数的this指向,即指向了按钮,即指向了下面的this,
    
    //等定时器解开的时候,会直接禁用this指向的按钮
    }.bind(this),3000) //这个this是在定时器外面,所以指向的是当前btn
}
</script>

柯里化

function sum(x,y,z){
    return x+y+z
}
var result=sum(1,2,3)
console.log(result)

上边的函数转为下面的函数的过程称为柯里化.

function sum(x){
    //x+=1  单一职责,即只操作x的业务.
    return function(y){
       //y-=2
       return function(z){
           //z*=3
           return x+y+z
      }
   }
}
//const sum=x=>y=>z=>{
//   return x+y+y
//}
var result=sum(1)(2)(3)
console.log(result)//6

应用场景:

  • 实现函数单一职责的原则
  • 逻辑复用
    • var re=sum(1)
      re(2)(3)
      re(4)(7)

       

高阶函数

满足下面任何一个条件都是高阶函数

  • 这个函数接收一个或多个函数作为输入
  • 这个函数输出一个函数
  • 这个函数同时接收和输出 (bind)

所以高阶函数的好处就是可以把函数任意的组合,然后返回一个新的函数。

//输入一个函数
var arr=[1,2,3]
//arr在排每一个数的时候都会回调fun这个函数
arr.sort(function(a,b){
    a-btn;
})
//上面函数等价于array.filter.call(array,fn) 接收一个数组,然后用fn对这个数组进行操作

//输入一个函数又输出一个函数
fn.bind.call(fn,{},1,2,3)

利用高阶函数简化求和代码

var arr=[2,3,4,5,65,6]
var sum=0
for(var i=0;i<arr.length;i++){
    if(arr[i]%2==0){
        sum+=arr[i]
    }
}
console.log(sum)

//利用高阶函数求偶数和////////////////////////

arr.filter(function(n){  //返回一个新的数组[2,4,6]
    n%2===0
}).reduce(function(left,next){
    return left+next}  //每次返回前一个数组和后一个数组
})

//纯高阶函数求偶数和
reduce(filter(function(n){
    n%2===0  //先找出有哪些数是偶数
}),function(left,next){ //再把这个运算规则放到这些偶数数组上
    return left+next
},0) //初始值是0

回调

回调函数就是另一个函数的参数。和异步没有任何关系。


构造函数和原型链

如果返回的是一个对象,它就是一个构造函数。

function Empty(){
    this.name='zzl'
    // return this 也可以不写,当new时,js会自动补上这句话
}
//下面一坨可以换成下下面的一行,即覆盖掉原来的prototype,但是js内置对象不能这样使用
//Empty.prototype={
 //   constructor: Empty, //这个必有,不然找不到共有属性,用于记录对象引用于(指向)哪个构造函数
 //   active:'跑', //此时就可以自己添加共有属性了
//    ...
//}

//prototype是函数的共有属性,可以自己添加属性,名称是Js默认的
Empty.prototype.active='跑' //或者可以直接这样写,就不用再写constructor了,因为本来就是默认有的
var empty=new Empty() //不用new,就要用Empty.call({})
//empty.name就等于了zzl //这个是实例成员,通过构造函数内部this添加到,只能用对象来访问
//构造函数没有参数,那么new的时候可以省略()

//此时的empty.active='跑' //因为Empty有个prototype共有属性,active='跑',
而empty里的__proto__指向了构造函数的原型对象prototype上面,所以empty也有了active属性
//在构造函数本身上添加的成员,就是静态成员,只能通过构造函数来访问,如下面所示
Empty.age='18'
console.log(Empty.age) //才能访问到18

__proto__ = Empty.prototype 不要代码中这样写
即__proto__不能直接赋值
而且浏览器控制台显示的__proto__的值不准确,不能参考,

原型是一个对象,protutype就是一个原型对象,作用是共享公共属性和方法。

每个对象身上会给自己添加一个__proto__属性,__proto__指向构造函数的原型对象prototype

它们两个本身是等价的,

原型链

function Star(uname,age){
    this.uname=uname;
    this.age=age;
}
Star.prototype.sing=function(){
    console.log('唱歌')
}
var ldh=new Star('刘德华',18);

成员查找机制

  • 当访问一个对象的属性和方法时,首先这个对象自身有没有该属性
  • 如果没有就查找它的原型(即__proto__指向的prototype原型对象)
  • 如果还没有就查找对象的原型(Object的原型对象)
  • 依次类推一直找到Object位置(null)

继承

ES6之前没有提供extends继承,只能通过 构造函数+原型对象 来模拟实现继承。

//借用父构造函数继承属性 
//父构造函数
function Father(uname,age){
    //this指向父构造函数的对象实例
    this.uname=uname;
    this.age=age;
}
//子构造函数
function Son(uname,age){
    //this指向子构造函数的对象实例
    //这样父类的this就改成了Son,继而相当于子类可以用父类构造函数的属性了
    Father.call(this,uname,age);
}
var son=new Son('zzl',18);
console.log(son);

利用原型对象继承方法

Son.prototype=Father.prototype;

不能这样赋值,因为,如果修改了子原型对象,父原型对象也会跟着一起变化。

下面是正确方法。

son.prototype=new Father(); //创建一个实例化对象Father,让son指向这个实例化对象;
而这个实例对象的__proto__会指向Father原型对象,这是构造函数的特点
//如果利用对象的形式修改了原型对象,别忘了重新指回原来的构造函数
son.prototype.constructor=Son;

son指向的是新的实例对象,所以对Father对象不影响。

构造函数的特点

  • 构造函数有原型对象prototype
  • 构造函数原型对象prototype里面有constructor指向构造函数本身
  • 构造函数可以通过原型对象添加方法
  • 由构造函数创建的Father实例对象(Son.prototype=new Father())都有__proto__,且指向构造函数的原型对象Father.Prototype。


箭头函数

const fn=( ) => {console.log(123)}
//小括号用来放形参,大括号代表函数体
fn();

它不支持this,也无法指定this,它始终是外面的this,如果传this,它会直接吃掉thiss什么也不显示;当然了它也没有arguments。

function fn(x,y){
    return x+y
}
//改为箭头函数
var fn=(x,y)=>{return x+y}

setTimeout(function(a){
    console.log(this)

    //由于箭头函数没有this,所以此时的this就是外面的this
    setTimeout((a)=>console.log(this),1000) 
}.bind({name:'zzl'}),1000)
可以去看阮一峰讲解的箭头函数

创建类

class name{
}
//创建实例
var x=new nname();

JS类的本质还是函数,可以认为就是构造函数

类constructor构造函数

它用来接收参数,以及返回实例对象,通过new命令生成对象实例的时候,自动调用该方法。

class name{ 
        constructor(a){
        this.a=a;
        }
}
var data=new name('zzl');
//此时data.a='zzl'

类中添加方法

class name{ 
            constructor(a){ 
            this.a=a;
            
} 
             sing(b){
             console.log('唱歌:'+b)
             }
} 
var data=new name('zzl');
data.sing('情歌');

类的继承

class Father{
    constructor(){

    }
    money(){
        console.log(100);
    }
}
class Son extends Father{

}
var son=new Son();
son.money();

super

用于访问和调用父类上的函数,可以调用父类的构造函数,也可以调用父类的普通函数。

class Father{
    constructor(x,y){
        this.x=x;
        this.y=y;
    }
    sum(){
        console.log(this.x+this.y);
    }
}
class Son extends Father{
    constructor(x,y){
        super(x,y); //调用了父类中的构造函数,这样父类的sum方法才能接收到值
        this.x=x;   //super必须写在子类this之前调用
        this.y=y;
        //this.sonFang(); //也可以用构造函数自动调用自己的方法
    }
    //super.sum() 也可以在这直接调用父类的方法
    sonFang(){}
}
var son=new Son(1,2);
son.sum(); //3

this.btn.onclick=this.sum;   //此时的this指向的是btn,而不是Son,因为是btn调用了this,
而btn里面是没有x和y的

get和set

class data{
    get price(){
        console.log('价格属性被读取了');
        return '1000';
    }
    set price(newprice){
        console.log('价格属性被修改了')
    }
}
let s=new data();
console.log(s.price); //价格被读取了 1000
s.newprice='20';//价格属性被修改了

defineProperty

Object.defineProperty()的第三个参数是以对象形式{ }书写:
  • value:设置属性的值,默认为undefined
  • writable:值是否可以重写,默认为false
  • enumerable:目标属性是否可以被枚举(遍历),默认为false
  • configurable:目标属性是否可以被删除或者是否可以再次修改第三个参数里面的特性,默认为false

keys()

用于获取对象自身所有的属性,返回的是一个由属性名组成的数组。


闭包

闭包是指有权访问另一个函数作用域中变量函数,也就是一个作用域可以访问另一个函数内部的局部变量。闭包延伸了变量的作用范围。

普通闭包

 function fn(){
     var num=100;
     function fun(){
         console.log(num);
     }
     fun();
 }
 fn();
//此时就产生了闭包

另一种闭包:fn外面的作用域可以访问fn内部的局部变量

function fn(){
    var num=100;
    function fun(){
        console.log(num);
    }
    return fun; //返回了fun函数,这样也就直接把num给返回了

   //  return function(){
   //      console.log(num)//直接返回一个匿名函数
   //  }
}
var data=fn(); //这样就可以访问fn内部的局部变量了
data(); //输出访问到的局部变量,因为此时data里存的是fun这个局部函数

案例

//利用闭包的方式得到当前的li的索引号
for(var i=0;i<FileList.length;i++){
    //利用for循环创建4个立即执行函数
    (function(i){
        //这样就能保证点击li不会是循环完的i,它用的就是外部作用域的i
        lis[i].oncilick=function(){console.log(i)}
    })(i)//立即函数里面参数是i,它会传给function的形参
}

浅拷贝和深拷贝

  • 浅拷贝只是拷贝一层,如果是更深层次对象,那就只拷贝引用(地址)
  • 深拷贝拷贝多层,每一级别的数据都会拷贝。

浅拷贝

 var obj={
     id:1;
     msg:{
         age:18
     }
 };
 var o={};
 for(var k in obj){
     //k是属性名 obj[k]是属性值
     //把obj拷贝给o
     o[k]=obj[k];
 }
 console.log(o);
 o.msg.age=20; //此时也会修改obj里的age,因为它们的age是用一个引用地址
 console.log(obj);

// es6语法糖方法
Object.assign(o,obj); //把obj拷贝给o

深拷贝

//深拷贝拷贝多层,每一级别的数据都会拷贝
 var obj={
     id:1,
     msg:{
         age:18
     }
 };
 var o={};
//封装函数
function deepCopy(newobj,oldobj){
    for(var k in oldobj){
        //判断属性值属于哪种数据类型
        //获取属性值oldobj[k]
        //k是属性名 obj[k]是属性值
        var item=oldobj[k];
        //判断这个值是否是数组
        //因为数组也属于Object,所以要先写在前面排除
        if(item instanceof Array){
            newobj[k]=[];
            deepCopy(newobj[k],item) //把item拷贝给newobj[k]
        }else if(item instanceof Object){
            //判断这个值是否是对象
            newobj[k]=[];//相当于一层层拆开复杂数据类型
            deepCopy(newobj[k],item);
        }else{
            //属于简单数据类型
            newobj[k]=item;
        }
    }
}
deepCopy(o,obj);//把obj拷贝给o
console.log(o);
o.msg.age=20; //不会有影响
console.log(obj)

渣渣龙, 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:JS进阶-函数
喜欢 (8)

您必须 登录 才能发表评论!

(3)个小伙伴在吐槽
  1. 函数的父级作用域与定义(编译)位置有关系,与调用(运行)位置没有关系
    王学龙2022-05-14 14:29
  2. 但this是在函数调用的时候确定的,不是this的定义位置
    王学龙2022-05-16 14:15
  3. this指向优先级:new绑定>显示绑定(apply/call/bind)>隐式绑定(obj.foo())>默认绑定(foo())
    厚积薄发2022-05-18 10:44