日常记录(二)

139 阅读11分钟

仅作为日常记录。

判断数组是否含有某个元素

1.arr.indexOf()

在之前的时候用的最多的就是indexOf方法,如果数组里面含有这个元素,返回的就是元素下标。如果没有就返回-1。而且这个是放在if条件里,太多太多了。

2.arr.includs()

有点懵之前为什么不用includs,实在是太简单了有没有,而且返回值直接就是Boolean。含有就返回true,没有就是false

3.arr.find(fn)

想不到吧,find也能检查。额,其实也没什么想不到的,find里参数是一个函数,就像这么用 arr.find(item=>item>3) 嘿,连return都省了,其实这个更适合于条件查询,功能也更强大。

4.arr.findIndex(fn)

这就是find()方法的变种,就是返回的第一个符合条件的值的下标。就不多赘述了。


类数组

函数的参数arguments其实是一个类数组,嗯,我又确认了一下,就是arguments。 它不能调用数组的方法,它其中的属性是从0开始排的,最后还有callee(我不知道这个是啥)还有length。

还有我们用document.querySelectAll()获取的nodeList也是类数组。

用getElementByTagName()获取的HTMLCollection也是类数组

下面我们说说怎么把类数组转为数组。

  1. Array.form()

这个是我最先接触到的,在vue源码的编译部分就是用的Array.form把获取到的nodeList转为数组后进行进一步处理的。

  1. 展开符

这个更简单,[...arguments],就能用了。

  1. Array.prototype.slice.call()

这里就要说一下这两个方法了

arrayObj.slice(start, [end]) //数组的截取

call([thisObj[,arg1[arg2[[argN]]]]])

这里有多个方法的变种

可以是

var args = []; 
for (var i = 1; i < arguments.length; i++) { 
    args.push(arguments[i]);
}

也可以是

[].slice.call(arguments,0)
  1. Array.prototype.concat.apply([],arguments)

这个与上面的方法相似。


说说你对闭包的理解

其实很多人对闭包的理解都不同。红宝书里面对闭包的定义是如果一个函数能够访问另一个函数的内部变量,那这个函数就是闭包。

MDN里面对闭包的定义是如果一个函数能够访问到自由变量,那这个函数就是闭包。其中又涉及了一个自由变量的概念,我们把既不是这个函数内定义的变量也不是传进来的形参的变量叫做自由变量。其实说实话就是其他函数的变量。

一般我们认为一个函数如果执行完毕了,那这个函数里的局部变量就会被释放,但是如果我们返回了一个函数,或者一个对象,然后返回的这个东西对变量有引用,那这就释放不了了,这个变量就会被存在堆上,就是我们上面说的自由变量。

对了,忘了说其中涉及的一个概念了,就是作用域。

作用域我们怎么理解呢,我们知道上下文分为全局执行上下文和函数执行上下文

上下文在创建的时候会经历三个阶段:thisBinding , 词法环境,变量环境。

这里面的点又有很多,我们挑重点讲。函数执行上下文,在词法环境里,会有一个外部环境的引用,这个函数声明在哪里,他的外部环境(outer)就在哪里,所以哦我们会说函数的作用域要找函数声明在哪里,而不是在哪里引用。自然而然地,作用域链也就很明白了,如果一个变量在当前函数内没有,就在outer里找它爸爸,爸爸没有就去找他爷爷。就这样。

闭包我们经常用来保护私有变量,如果我们要修改这个变量,可以提供一个函数。

还用来模块封装。就像我们 export default Vue

然后我们引入这个模块之后就可以访问到这个Vue内部的函数。

当然如果我们在调用之后要想释放这个变量,只需要置空,因为现在浏览器是标记清除的,如果从root上找不到对这个变量的引用,浏览器就会自动回收。

好,这就是我现在基本上对闭包的理解。

再加一点,解决一个与闭包有关的问题

for(var a=0;a<6;a++){
    setTimeout(()=>{
        console.log(a)
    },0)
}

输出结果6个6,6不6 ? 这是因为在执行for循环的时候,就执行到setTimeout,所以就不会执行log.

对于这个问题我们可以提出来多种解决方案 1.把这个函数改成一个立即执行函数

for(var a=0;a<6;a++){
    (function(a){setTimeout(function(){
        console.log(a)
    },0)})(a)
}

2.将var 改成let

ES6引进来的let具有块级作用域,也就是说每一个a都是对应一个函数的,所以会正确的打印我们想要的。

3.给setTimeout传初始值

这个其实又涉及另外一个点了,就是常用函数的不常用参数。setTimeout的第三个参数是初始值。另外再多一句嘴。praseInt的第二个参数是redix,就是以几进制解析。

for(var a=0;a<6;a++){
    //注意setTimeout里面打印的是传进来的初始值
    setTimeout(a=>{
        console.log(a)
    },0,a)
}

好了,基本上就这样。实际问题实际分析。


call apply bind

本来这里是要写继承的几种方式的,但是写着写着才发现一定要先写一下this的指向,就是一开始函数上下文创建时的第一步,thisBinding。这里考察的点也是比较多。

我看this的绑定有好多种,像是默认绑定,隐式,显示绑定,new绑定,还有一个箭头函数的绑定。

其实记起来很简单,就看函数是怎么调用的,如果是函数调用,就是指向window,如果是方法调用,this就指向调用的对象。

如果是new,就指向新new出来的实例。当然了,待会我们要写一下new的实现方法。

箭头函数没有this,所以箭头函数里面的this.xxx就当成变量,从作用域找。

剩下的就是call一类的方法绑定了,绑谁this指向谁。不过最后的最后,也就是最重要的,看看函数是怎么调用的。

好了,this指向就说这么多。接下来谈一下call这几个方法。

call() 和 apply()

这两个方法基本一样,所以我们放在一起说,就是里面接收的参数不同。

常用方法:

1.合并两个数组

var arr1 = [1,2]
var arr2 = [3,4]
Array.prototype.push.apply(arr1,arr2)
arr1    //[1,2,3,4]

如果参数太大了,考虑将arr2切块分别push,不要直接push

2.获取数组的最大/小值

这其实就是用的Math的方法,一看便知。

var arr1 = [1,2,8,6]
Math.max.apply(Math,arr1)  //8

3.检查数据类型

Object.prototype.toString.call(target)

就这样,我们在第一篇的时候写过这个,不过这里的toString()不能是被重写之后的,不然就无效了。

4.类数组转为数组

Array.prototype.slice.call(arguments)

这个也说过,这个也相当于

[].slice.call(arguments)

讲一句。slice是将类似于数组的元素(通过下标)放到一个新的数组里。再有一个。类数组的存在意义是为了更快的处理数据

5.调用父构造函数实现继承

继承,嗯,这不就是我们下面要谈的吗?

不过,在这之前我们还是要写一下call()和apply()的基本实现。

call()和apply()实现的都是改变this指向,执行这个函数。

我们一步一步来,首先我们实现这两个功能。

先把调用call的函数挂在需要指向this的对象上,然后运行这个函数,然后再删除。

Function.prototype.call_ = function(context){
    context.fn = this;
    context.fn();
    delete context.fn
}

第二步,我们要优化一下,第一步的函数不能接收参数.

Function.prototype.call_ = function(context){
    context.fn = this; 
    var args = [...arguments].slice(1) //第一个参数是this,要删掉,不然无数层
    context.fn(...args);
    delete context.fn
}

第三步,我们需要对这个函数优化一下: 1.this的参数可以传null,undefined,此时this指向window,

2.函数可以有返回值。

3.this可以传基本数据类型,原生的call会自动用Object()转换。

Function.prototype.call_ = function(context){
    context = context?Object(context):window
    context.fn = this; 
    
    let args = [...arguments].slice(1) //第一个参数是this,要删掉,不然无数层
    let results = context.fn(...args);
    
    delete context.fn
    return results
}

好,基本上就是这样,然后我们写一个apply(),只是参数不同。

Function.prototype.apply_ = function(context,arr){
    context = context?Object(context):window
    context.fn = this; 
    
    let results;
    if(arr){
        results = context.fn(...arr)
    }else{
        results = context.fn()
    }
    
    delete context.fn
    return results
}

bind与call/apply最大的不同就是bind返回一个函数,而call/apply是直接执行了这个函数。

bind()有几个特性,我们实现的时候需要重点把握的

1.可以指定this

2.可以返回一个函数

3.可以传入参数

4.柯里化,柯里化的定义就是只给函数传递一部分参数来调用他,然后返回一个函数去处理剩下的参数。这个感觉起来就像一级级的权限管理。

为了方便理解我们写一个例子。

var add  =function(a){
    return function(b){
        return a+b
    }
}

var addOne = add(1)
var addTen = add(10)

addOne(2) //3
addTen(15) //25

有没有发现bind也是闭包的一种使用场景呢?

接下来我们实现一下,也是分步走。

我们先用call/apply指定this,然后返回一个函数

Function.prototype.bind_ = function(context){
    var self = this
    return function (){
        return self.call(context)
    }
}

第二步我们加对参数的处理。

Function.prototype.bind_ = function(context){
    var self = this
    var args = [...arguments].slice(1) //与call是一样的
    
    return function (){
        var bindArr = [...arguments]
        return self.apply(context,args.concat(bindArr))
    }
}

这样大部分的功能其实已经完成了,不过我们注意到bind还有一个特性

一个绑定函数也可以使用new来创建对象,这种创建的对象里面的this丢失,不过参数正常传递。

意思就是这么个意思,我可能表述的不那么准确

Function.prototype.bind_ = function(context){
    var self = this
    var args = [...arguments].slice(1) //与call是一样的
    
    var fBound =  function (){
        var bindArr = [...arguments]
        //如果作为构造函数,指向this。普通函数指向context
        return self.apply(this instanceof fBound?this:context,args.concat(bindArr))
    }
    //可以继承绑定函数原型中的值
    fBound.prototype = this.prototype
    return fBound
}

下面我们进行第四步优化,因为修改fBound.prototype的时候this.prototype也会被修改。 解决方案是使用一个空对象为中介。

还有一步我们一起加上去,就是不是函数的抛错。

Function.prototype.bind_ = function(context){
    if(typeof this !== 'function'){
        throw new Error('输入个函数')
    }
    
    var self = this
    var args = [...arguments].slice(1) //与call是一样的
    
    var fNop = function(){}
    
    var fBound =  function (){
        var bindArr = [...arguments]
        //如果作为构造函数,指向this。普通函数指向context
        return self.apply(this instanceof fNop?this:context,args.concat(bindArr))
    }
    //可以继承绑定函数原型中的值
    fNop.prototype = this.prototype
    fBound.prototype = new fNop()
    return fBound
}

继承的几种方式

继承的概念还是挺广的,如果可以通过某种方式可以让某个对象访问到其他对象的属性和方法,那这个就是继承。

当然从这个概念上来说,拷贝也是继承。

1.原型链继承,先改变原型对象,然后构建实例。

function Person(name,age){
    constructor:Person,
    this.name = name
    this.age = age
    this.say = function(){
        console.log("say something")
    }
}

var p1 = new Person()
var p2 = new person()

p1.say() === p2.say() //false

new出来之后两个say()指向不同的内存,造成内存浪费。

要解决这个问题我们可以在Person的原型对象中添加方法,这样指向的就是一个内存

Person.prototype.run = function(){
    console.log('running')
}
var p1 = new Person()
var p2 = new person()

p1.run() === p2.run() //true

另外说一点,Person.prototype 是构造出来实例的原型对象

而Function.prototype是Person的原型对象

所以原型对象就找是谁构造他的,构造函数的prototype.

而当我们需要添加的方法很多的时候,使用Person.prototype逐个添加方法又太low了,所以我们改进一下。

Person.prototype = {
    run(){},
    eat(){}
}
var p1 = new Person()
p1.run()

2.拷贝继承 (混入继承)

有时候我们想修改某个对象中的属性,但是又不能直接修改它,所以我们可以创建一个该对象的拷贝。

var p1 = {name:'link',age:24}
var p2 = {}
for(var key in p1){
    p2[key] = p1[key]
}
p2.age = 25

对于拷贝继承又引出了深浅拷贝这个问题。。我们稍后会专门写出深浅拷贝的实现。

3.原型式继承

这个一般有两种方法,一个是我们常见的创建一个空对象:Object.create(null)

另一个是创建一个继承某个父对象的子对象。

var parents = {name:'link',age:24}
var child = Object.create(parents) //这个方法跟第一种没什么区别,只是更加简单

上面是继承的基础,下面是需要你理解


继承这部分其实我不太了解,只能写着然后发散,等有时间要回来重温一下。而且这里面有设计思想一类的东西。需要掌握。

1.使用call

//使用call来继承
function P(){
    this.papa = 'baba'
    function dad(){
        this.dad = 'dad'
    }
}

function C(){
    P.call(this)
    this.son = 'son'
}

console.log(new C())   //C {papa: "baba", son: "son"}

这里的使用call()来继承有问题,就是爸爸的函数儿子继承不到。

2.使用原型链继承