阅读 3998

前端面试题 回顾与复习(更新中)

原生DOM操作

  • 如需替换 HTML DOM 中的元素,请使用replaceChild(newnode,oldnode)方法
  • 从父元素中删除子元素 parent.removeChild(child);
  • insertBefore(newItem,existingItem) 在指定的已有子节点之前插入新的子节点
  • appendChild(newListItem向元素添加新的子节点,作为最后一个子节点 

事件模型

事件传播指的是发生事件时传播的过程。一共按顺序分为以下三个阶段。  

捕获阶段:从window对象传导到目标节点(从上到下)的过程,直到目标的元素,为截获事件提供机会

目标阶段:在当前目标触发的过程 ,目标接受事件

冒泡阶段:从目标节点传导回到windowd对象(从下到上)的过程,在这个阶段对事件做出响应。


深拷贝与浅拷贝(前端基础面试高频面试题)

参考链接 https://juejin.im/post/59ac1c4ef265da248e75892b#heading-2

引子:

Js里有两种数据类型,基本数据类型和引用数据类型。深拷贝、浅拷贝一般都是针对引用数据类型的。

基本数据类型主要是:undefined,boolean,number,string,null

var a = 1;
var b = a;
a = 2;
console.log(a); // 2
console.log(b); // 1复制代码
对于基本数据类型赋值操作,b 复制了 a 的值,而不是引用。即保存 b 的值与 a 的值的内存空间是完全独立的。
var arr1 = [1,2,3,4];
var arr2 = arr1;

arr1.push(5);
console.log(arr1); // [1,2,3,4,5]
console.log(arr2); // [1,2,3,4,5]

arr2.push(6);
console.log(arr1); // [1,2,3,4,5,6]
console.log(arr2); // [1,2,3,4,5,6]复制代码

然而,对于引用数据类型的赋值操作,arr2 仅仅是复制了 arr1的引用(也可以称之为指向 arr1 内存地址的指针)。简单来说,就是 arr1 与 arr2 指向了同一个内存空间

浅拷贝:

如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

function shallowCopy(copyTarget) {
    var obj = {};
    for (var key in copyTarget) {
        obj[key] = copyTarget[key];
    }
    return obj;
}

var json1 = {
    'name': '张三',
    'family': {
        'children': '张三三',
        'wife': '李四'
    }
}

var json2 = shallowCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '张一'
console.log(json1);
console.log(json2);复制代码


由此可以看出,浅拷贝仅仅拷贝了基本类型的数据,对于引用类型数据,则指向被复制的内存地址,若原地址中的对象发生改变,那么浅复制出来的对象也会相应改变。

深拷贝:

深拷贝可概括为:为引用类型数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

function deepCopy(copyTarget) {
    var obj = {};
    for(var key in copyTarget) {
        // 先判断obj[key]是否为对象
        if(typeof copyTarget[key] === "object"){
            // 递归
            obj[key] = deepCopy(copyTarget[key]);
        } else {
            // 如果不是对象,直接赋值即可
            obj[key] = copyTarget[key];
        }
    }
    return obj;
}

var json1 = {
    'name': '张三',
    'family': {
        'children': '张三三','wife': '李四'
    }
}

var json2 = deepCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '张一'
console.log(json1);
console.log(json2);复制代码


拓展

深复制可以用JSON的方式:JSON.parse(JSON.stringify(obj))

但是JSON复制会忽略掉值为undefined以及函数表达式。

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); //Object {a: 1, b: 2}复制代码


事件环(个人认为是比较高级的面试题)

参考链接 juejin.im/post/5b35cd…

浏览器中 Event Loop:

浏览器中, js引擎线程会循环从 任务队列 中读取事件并且执行, 这种运行机制称作 Event Loop (事件循环).

每个浏览器环境,至多有一个event loop。 一个event loop可以有1个或多个task queue(任务队列) 先执行同步的代码,然后js会跑去消息队列中执行异步的代码,异步完成后,再轮到回调函数,然后是去下个事件循环中执行setTimeout 它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task(微任务)。当所有可执行的micro-task(微任务)执行完毕之后。循环再次从macro-task 宏任务开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。  

从规范上来讲,setTimeout有一个4ms的最短时间,也就是说不管你设定多少,反正最少都要间隔4ms才运行里面的回调。而Promise的异步没有这个问题。Promise所在的那个异步队列优先级要高一些 Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的 Promise的任务会在当前事件循环末尾中执行,而setTimeout中的任务是在下一次事件循环执行

总结:

  • 我们的同步任务在主线程上运行会形成一个执行栈
  • 如果碰到异步任务,比如setTimeout、onClick等等的一些操作,我们会将他的执行结果放入队列,此期间主线程不阻塞
  • 等到主线程中的所有同步任务执行完毕,就会通过event loop在队列里面从头开始取,在执行栈中执行
  • event loop永远不会断
  • 以上的这一整个流程就是Event Loop(事件循环机制)
  • setTimeout(function(){
        console.log(4)
    },0);
    new Promise(function(resolve){
        console.log(1)
        for( var i=0 ; i<10000 ; i++ ){
            i===9999 && resolve()
        }
        console.log(2)
    }).then(function(){
        console.log(5)
    });
    console.log(3);
    //依次输出 1  2  3  5  4
    复制代码

    vue路由传参

    通过Vue传递参数可以分为两种方式: params参数 query参数

    params参数传递方式分两种:(1)通过router-link进行跳转 (2)通过编程导航进行路由跳转


    params传值 


    <div
      v-for="item in list"
      @click="getDescribe(item)"
    >
      {{item}}
    </div>复制代码
    data() {
        return {
            list: ['101','102','103']
        }
    }复制代码

    对应路的由配置如下:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }复制代码

    push 后面可以是对象,也可以是字符串:

    对象:

    getDescribe(item) {
        this.$router.push({
            path: `/two/${item}`
        })
    }复制代码

    字符串:

    getDescribe(item) {
        this.$router.push(`/two/${item}`)
    }复制代码

    命名的路由:

    getDescribe(item) {
        this.$router.push({ name: 'two', params: { id: `${item}` }})
    }复制代码

    注意: 需要注意的是使用params必须和name属性一起使用,否则要跳转的目标路由页面无法通过params获取到传递过来的参数。params: 相当于 post 请求,请求参数不会体现在地址栏中。 这种方法会出现如下问题: 如果子页面点击【刷新】按钮时,刚才传过来的参数并不会同步。因为参数没有同步到网页地址栏中。但是如果要让他体现在地址栏中,可以在配置路由时,写在path属性中。 如下可以解决刷新的问题:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }复制代码

    读取参数:

    console.log(this.$route.params)复制代码


    query传值。 类似get传值

    query: 相当于 get 请求,请求参数会体现在地址栏中。

    this.$router.push({
        path: `/two`,
        query: {
            id:item
        }
    })复制代码

    http://localhost:8888/#/two?id=101

    console.log(this.$route.query)复制代码

    this.$router 和this.$route有何区别?

    上面提到的编程式导航中用this.$router.push()来改变路由,用this.$route.params来得到参数值

    1.$router为VueRouter实例,想要导航到不同URL,则使用$router.push方法。

    2.$route为当前router跳转对象,里面可以获取name、path、query、params等


    vue中watch&&computed 

    computed特性 

    1.是计算值, 

    2.应用:就是简化tempalte里面{{}}计算和处理props或$emit的传值 

    3.具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数  

    注意:computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算

    watch特性 

    1.是观察的动作, 

    2.应用:监听props,$emit或本组件的值执行异步操作 

    3.无缓存性,页面重新渲染时值不变化也会执行

    注意:watch在每次监听的值变化时,都会执行回调。其实从这一点来看,都是在依赖的值变化之后,去执行回调

    总结:如果一个值依赖多个属性(多对一),用computed肯定是更加方便的。如果一个值变化后会引起一系列操作,或者一个值变化会引起一系列值的变化(一对多),用watch更加方便一些。

    watch的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作。computed通常就是简单的计算。 watch和computed并没有哪个更底层,watch内部调用的是vm.$watch,它们的共同之处就是每个定义的属性都单独建立了一个Watcher对象

    Vue.js 提供了一个方法 watch,它用于观察Vue实例上的数据变动。

    <template>
      <div>
        <input
          type="text"
          v-model="age"
        >
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    age:18
                }
            },
            watch: {
                age: (newAge,oldAge) => {
                    console.log('新值为:'+newAge+',旧值为:'+oldAge);
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {        
            }
        }
    复制代码

    如何理解Virtual DOM

    一、vdom是什么? 

    vdom是虚拟DOM(Virtual DOM)的简称,指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做。换而言之,vdom就是JS对象。

    如下真实DOM

    <ul id="list">
        <li class="item">Item1</li>
        <li class="item">Item2</li>
    </ul>复制代码

    映射成虚拟DOM就是这样:

    {
        tag: "ul",
        attrs: {
            id:&emsp;"list"
        },
        children: [
            {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item1"]
            }, {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item2"]
            }
        ]
    } 复制代码

    二、为什么要用vdom?

    采用JS对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。

    当然有人说虚拟DOM并不比真实的DOM快,其实也是有道理的。当一个table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在js中diff算法的比对过程。所以,性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。

    虚拟DOM更加优秀的地方在于: 

    1、它打开了函数式的UI编程的大门,即UI = f(data)这种构建UI的方式。  

    2、可以将JS对象渲染到浏览器DOM以外的环境中,也就是支持了跨平台开发,比如ReactNative。

    三、diff算法


    vue 组件之间的通信

    1、父组件向子组件通信

    使用props,父组件可以使用props向子组件传递数据。

    father.vue

    <template>
      <div>
        <Child :name="msg"></Child>
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    msg: 'feixuan'
                }
            },
            computed: {
    
            },
            mounted() {
    
            },
            methods: {
            }
        }
    </script>复制代码

    child.vue

    <template>
      <div>
        {{name}}
      </div>
    </template>
    
    <script>
        export default {
            props:{
                name:{
                    type:String,
                    default:''
                }
            },
            data() {
                return {
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            },
            methods: {
            }
        }
    </script>复制代码

    2、子组件向父组件通信

    方法一: 使用vue事件 父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件。

    father.vue

    <template>
      <div>
        <Child @msgFunc="msgEvent"></Child>
        {{message}}
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    message: ''
                }
            },
            methods: {
                msgEvent(msg) {
                    console.log(msg)
                    this.message = msg
                }
            },
            computed: {
    
            },
            mounted() {
    
            }
            
        }
    </script>复制代码

    child.vue

    <template>
      <div>
        <button @click="handleClick">点我</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                }
            },
            methods: {
                handleClick() {
                    this.$emit('msgFunc','我是来自子组件的消息');
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            }
            
        }
    </script>复制代码

    3、非父子组件通信

    对于两个组件不是父子关系,那么又该如何实现通信呢?在项目规模不大的情况下,完全可以使用中央事件总线 EventBus 的方式。如果你的项目规模是大中型的,那么可以使用vuex状态管理

    EventBus 通过新建一个 Vue 事件 bus 对象,然后通过 bus.$emit 触发事件,bus.$on 监听触发的事件。

    Vue.js异步更新DOM策略及nextTick

    在使用vue.js的时候,有时候因为一些特定的业务场景,不得不去操作DOM,比如这样:

    <template>
      <div>
        <div ref="test">{{test}}</div>
        <button @click="handleClick">点击</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    test: 'begin'
                }
            },
            methods: {
                handleClick () {
                    this.test = 'end';
                    console.log(this.$refs.test.innerText);//打印“begin”
                }
            },
            components: {
            },
            computed: {
            },
            mounted() {
            }
        }
    </script>
    复制代码

    打印的结果是begin,为什么我们明明已经将test设置成了“end”,获取真实DOM节点的innerText却没有得到我们预期中的“end”,而是得到之前的值“begin”呢?

    原因

    Vue.js源码的Watch实现。当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。我们来看一下update的实现。

    update () {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            /*同步则执行run直接渲染视图*/
            this.run()
        } else {
            /*异步推送到观察者队列中,下一个tick时调用。*/
            queueWatcher(this)
        }
    }复制代码

    Vue的双向数据绑定

    Vue是一个MVVM框架,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化。

    实现数据绑定的方式大致有以下几种:

    - 1、发布者-订阅者模式(backbone.js)
    - 2、脏值检查(angular.js)
    - 3、数据劫持(vue.js)复制代码

    Vue.js则是通过数据劫持以及结合发布者-订阅者来实现的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。


    八、十、十六进制转换

    二进制 → 十进制

    方法:二进制数从低位到高位(即从右往左)计算,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

    例:将二进制的(101011)B转换为十进制的步骤如下:
    
    1. 第0位 1 x 2^0 = 1;
    
    2. 第1位 1 x 2^1 = 2;
    
    3. 第2位 0 x 2^2 = 0;
    
    4. 第3位 1 x 2^3 = 8;
    
    5. 第4位 0 x 2^4 = 0;
    
    6. 第5位 1 x 2^5 = 32;
    
    7. 读数,把结果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。
    复制代码

    八进制 → 十进制

    方法:八进制数从低位到高位(即从右往左)计算,第0位的权值是8的0次方,第1位的权值是8的1次方,第2位的权值是8的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。   八进制就是逢8进1,八进制数采用 0~7这八数来表达一个数。

    例:将八进制的(53)O转换为十进制的步骤如下:
    
    1. 第0位 3 x 8^0 = 3;
    
    2. 第1位 5 x 8^1 = 40;
    
    3. 读数,把结果值相加,3+40=43,即(53)O=(43)D。复制代码

    十进制 → 二进制

    方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。 

    例:将十进制的(43)D转换为二进制的步骤如下:
    
    1. 将商43除以2,商21余数为1;
    
    2. 将商21除以2,商10余数为1;
    
    3. 将商10除以2,商5余数为0;
    
    4. 将商5除以2,商2余数为1;
    
    5. 将商2除以2,商1余数为0; 
    
    6. 将商1除以2,商0余数为1; 
    
    7. 读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,101011,即(43)D=(101011)B。复制代码


    函数节流跟防抖

    函数防抖(debounce)

    防抖函数 debounce 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间 3 秒的函数,在这 3 秒内如果遇到函数调用请求就重新计时 3 秒,直至新的 3 秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。


    原理及实现 :

    实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。


    函数节流(throttle)

    规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效,
    即每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,
    通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
    复制代码


    总结 :

    函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

    结合应用场景 

    debounce : 

    search搜索联想,用户在不断输入值时,用防抖来节约请求资源。 window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次 

    throttle :

    鼠标不断点击触发,mousedown(单位时间内只触发一次) 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断  


    斐波那契数列

    斐波那契数列也叫黄金分割数列,也叫兔子数列 

    原理:假定一对大兔子每月能生一对小兔子,且每对新生的小兔子经过一个月可以长成一对大兔子,如果不发生死亡,且每次均生下一雌一雄,问一年后共有多少对兔子?

    月份兔子情况总数
    第0个月a(小兔子)1
    第1个月a(具备繁殖能力)1
    第2个月b(生啦生啦)+ a(他父母)2
    第3个月b(2月份出生的具备繁殖能力,正跃跃欲试) + b2(他父母又生二胎了) +a(他父母)3
    第4个月c(2月份的兔子b喜当爹)+b(二月份出生的兔子) + b2(二胎具备繁殖能力,准备生娃) +a(他父母)+d(a生三胎)5

    1、1 、2、3、5、8、13、21、34、55、89…… 

    所以规律就是 fn(n)=fn(n-1)+fn(n-2)

    迭代 方式

    /*
    *i 月份
    */
    function fn(i){
        var a=[];
        /*0个月什么都不存在*/
        a[0]=0;
        a[1]=1;
        for(var j = 2; j<= i;j++){
            a[j]=a[j-1] + a[j-2];
        }
        return a[i]
    }复制代码

    递归方式

    /*
    * i 月份
    */
    function fn(i){
        if(i < 2){return i === 0 ? 0 : 1;}
        return fn(i-1)+fn(i-2)
    }复制代码

    总结: 针对这个例子来说,这里的递归会进行太多次的调用(比迭代多),所以简洁的背后牺牲的是性能


    vue-router实现原理

    这个是阿里的一道面试题

    核心原理: 更新视图但不重新请求页面。

    vue-router实现单页面路由跳转,提供了三种方式:hash模式、history模式、abstract模式,根据mode参数来决定采用哪一种方式。

    路由模式

    vue-router 提供了三种运行模式: 

    hash: 使用 URL hash 值来作路由。默认模式。 

    history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。 

    abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端

    Hash模式:

    hash即浏览器url中#后面的内容,包含#。hash是URL中的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会加载相应位置的内容,不会重新加载页面。 也就是说 即#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。 每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。 所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。

    History模式:

    HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面; 由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。 有时,history模式下也会出问题: eg: hash模式下:xxx.com/#/id=5 请求地址为 xxx.com,没有问题。 history模式下:xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误; 为了应对这种情况,需要后台配置支持: 在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

    abstract模式:

    abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。 根据平台差异可以看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)。


    观察者模式

    异步编程

    async 及 await

    async 是让方法变成异步。 await 是等待异步方法执行完成。

    注意:await 必须在 async 方法中才可以使用因为await 访问本身就会造成程序停止堵塞,所以必须在异步方法中才可以使用

    使用await语法的时候,后面的函数需要返回Promise对象

    async test1() {
        let result = await this.$http.get('/api/job',{})
        console.log(result)
    }复制代码

    十大经典排序算法

    冒泡排序:

    两两元素进行比较,较大者放置后头,类似水中的气泡较大者浮在水的上端 

    参考:juejin.im/post/5d20ab…

    function bubble(arr) {
      for(var i = 0; i < arr.length; i++) {
        for(var j = 0; j < arr.length - 1- i; j++) {
          if(arr[j] > arr[j + 1]) {
            var max = arr[j];
            arr[j] = arr[j+1];//交换数据
            arr[j+1] = max;//交换数据
          }
        }
      }
      console.log(arr)
    }
    
    bubble([1,4,6,2,7,2])// [1, 2, 2, 4, 6, 7]复制代码

    快速排序:

    取一个元素为基准,把序列分成两部分,小于基准的放到它的左面,大于等于的放到它的右面,然后在把左面和右面的子序列再进行上述的拆分,直到子序列不可再分割(小于2个元素),最终达到整个序列有序 

    paste image


    function quickSort(arr) {
      if(arr.length < 2) {
        return arr;
      } else {
        const pivot = arr[0]; // 基准值
        const pivotArr = []; // 一样大的放中间
        const lowArr= []; // 小的放左边
        const hightArr = []; // 大的放右边
        arr.forEach(current => {
          if(current === pivot) pivotArr.push(current);
          else if(current > pivot) hightArr.push(current);
          else lowArr.push(current);
        })
        return quickSort(lowArr).concat(pivotArr).concat(quickSort(hightArr));
      }
    }
    
    console.log(quickSort([4,6,2,3,1,5,7,8])) // [1, 2, 3, 4, 5, 6, 7, 8]复制代码

    简单选择排序:

    首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 重复第二步,直到所有元素均排序完毕。

    const selectionSort = array => {
      const len = array.length;
      let minIndex, temp;
      for (let i = 0; i < len - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
          if (array[j] < array[minIndex]) {
            // 寻找最小的数
            minIndex = j; // 将最小数的索引保存
          }
        }
        temp = array[i];
        array[i] = array[minIndex];
        array[minIndex] = temp;
        console.log('array: ', array);
      }
      return array;
    };
    selectionSort([6,4,3,8])//[3, 4, 6, 8]复制代码


    async和defer

    异步加载js有三种 : defer 、 async 、 动态创建script标签 、 按需异步载入js

    async : 并行加载脚本文件,下载完毕立即解释执行代码,不会按照页面上的script顺序执行。 defer : 并行下载js,会按照页面上的script标签的顺序执行,然后在文档解析完成之后执行脚本

    defer 与 async 的相同点是采用并行下载,在下载过程中不会产生阻塞。区别在于执行时机,async 是加载完成后自动执行,而 defer 需要等待页面完成后执行。

    解析:

    <script src="script.js"></script>

    没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

    <script async src="script.js"></script>

    有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

    <script defer src="myscript.js"></script>

    有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

    Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

    call、apply、bind方法的使用

    call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法,但是其他的有,我们可以借助 call 或 apply 用其它对象的方法来操作。

    知乎简单易懂答案:
    猫吃鱼狗吃肉,奥特曼打怪兽
    有一天,狗想吃鱼了
    猫.吃鱼.call(狗,鱼)
    狗就吃到鱼了
    猫成精了,想打怪兽
    奥特曼.打怪兽.call(猫,小怪兽)
    
    obj.call(thisObj, arg1, arg2, ...)
    obj.apply(thisObj, [arg1, arg2, ...])
    
    两者作用一致,都是把obj(即this)绑定到thisObj,这时候thisObj具备了obj的属性和方法。
    或者说thisObj继承了obj的属性和方法。唯一区别是apply接受的是数组参数,call接受的是连续参数。复制代码

    例子:

    var obj1 = {
      value: 1
    }
    
    function say() {
      console.log(this.value)
    }
    
    say() // 输出undefined
    say.call(obj1) // 输出1复制代码

    注意两点 : call 改变了 this 的指向,此时的 this 指到了 obj1 say 函数执行了

    bind方法的特殊性:

    之所以把bind方法单独放出来是因为bind方法和前面两者还是有不小的区别的,虽然都是动态改变this的值,举个例子

    var obj = {
        x: 81,
    };
    
    var foo = {
        getX: function() {
            return this.x;
        }
    }
    console.log(foo.getX.bind(obj)());  //81
    console.log(foo.getX.call(obj));    //81
    console.log(foo.getX.apply(obj));   //81复制代码

    有没有注意到使用bind方法时候后面还要多加上一对括号,因为使用bind只是返回了对应函数并没有立即执行,而call和apply方法是立即执行的


    Vue 的生命周期

    beforeCreate:

    vue实例的挂载元素el和数据对象data都为undefined,还未初始化

    created:

    实例创建完成 vue实例的数据对象data有了,el还没有。并已经完成以下配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调
    此时可以调用methods中定义的方法,修改data的数据,并且可触发响应式变化、computed值重新计算,watch到变更等

    还未挂载到DOM,不能访问到$el属性,$ref属性内容为空数组

    beforeMount: 

    vue 实例的$el 和 data 都初始化了,但还是挂载之前为虚拟的 dom 节点,data.message 还未替换

    mounted:

    实例挂载到DOM上,此时可以通过DOM API获取到DOM节点,$ref属性可以访问,data.message 成功渲染

    beforeUpdate:  

    这里的更新对象是模板,即需要虚拟 DOM 重新渲染和打补丁,beforeUpdate发生在以上两个流程之前,此时新的虚拟DOM已经生成

    如果发生变更的数据在模板中并没有使用(包括直接和间接,间接:比如某个依赖该数据的计算属性在模板中使用了),则不会触发更新流程!!!

    updated: 

    由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

    当这个钩子被调用时,组件 DOM 已经更新,可以执行依赖于 DOM 的操作

    beforeDestroy: 

    实例销毁之前调用。在这一步,实例仍然完全可用,this仍能获取到实例

    一般在这一步中进行:销毁定时器、解绑全局事件、销毁插件对象等操作

    destroyed:

    Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁


    闭包

    参考 https://segmentfault.com/a/1190000000652891

    闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

    要理解闭包,首先必须理解Javascript特殊的变量作用域。

    变量的作用域无非就是两种:全局变量和局部变量。

    Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

      var n = 999;
      function f() {
         console.log(n)
       }
     f(); // 999     
    
    复制代码

    另一方面,在函数外部自然无法读取函数内的局部变量。

    function f() {
       var n= 999;
    }
    console(n); //Uncaught ReferenceError: n is not defined
    
    复制代码

    这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

    function f() {
        n = 999;
    }
    f();
    console.log(n); // 999复制代码

    出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

    那就是在函数的内部,再定义一个函数。

    function f1() {
        var n = 999;
        function f2(){
             console.log(n); // 999
        }
    }复制代码

    在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),

    子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

    既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

    function f1() {
       n = 999;
         function f2(){
              console.log(n);
         }
       return f2;
    }
    var result=f1();
    result();// 999复制代码

    闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中

    function f1() {
        var n = 999;
        nAdd = function() {
            n += 1
         }
         function f2() {
           console.log(n);
         }
         return f2;
    
     }
     var result = f1();
     result(); // 999
     nAdd();
     result(); // 1000复制代码

    在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

    为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个

    匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

    总结

    特性: 

    1.函数嵌套函数 

    2.函数内部可以引用外部的参数和变量 

    3.参数和变量不会被垃圾回收机制回收

    闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

    为什么要使用闭包:

    为了设计私有方法和变量,避免全局变量污染 希望一个变量长期驻扎在内存中

    数组去重

    利用数组的 indexOf 属性

    let arr = [5,7,8,8]
    console.log(arr.indexOf(5))//0
    console.log(arr.indexOf(8))//2
    console.log(arr.indexOf(9))//-1
    
    function unique(origin) {
        var result = [];
        for (var i = 0; i < origin.length; i++) {
            var item = origin[i];
            if (result.indexOf(item) === -1) {
                result.push(item);
            }
        }
        return result;
    }
    console.log(unique(arr))//[5, 7, 8]复制代码

    数组的 filter 属性和 indexOf 属性

    let arr = [5,7,8,8]
    function unique(origin) {
      var result = origin.filter(function(item, index, array) {
        // 获取元素在源数组的位置,只返回那些索引等于当前元素索引的值。
        return array.indexOf(item) === index;
      });
      return result;
    }
    console.log(unique(arr))//[5, 7, 8]复制代码

    利用 ES6 Set

    ES6 提供了新的数据结构 Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。向 Set 加入值的时候,不会发生类型转变,所以 5 和 ‘5’ 是两个不同的值。Set 内部判断两个值是否相同,用的是类似于 “===”的算法,但是区别是,在 set 内部认为 NaN 等于 NaN 

    let arr = [5,7,8,8]
    function unique(origin) {
      return Array.from(new Set(origin));
    }
    console.log(unique(arr))//[5, 7, 8]复制代码


    盒子模型

    首先盒子模型是由content、padding、border、margin 4部分组成

    Box-Model

    分类: 标准盒子模型 和 ie盒子模型

    区别:

    • 在 标准盒子模型中,width 和 height 指的是内容区域的宽度和高度。增加内边距、边框和外边距不会影响内容区域的尺寸,但是会增加元素框的总尺寸。
    • IE盒子模型中,width 和 height 指的是内容区域+边框+内边距的宽度和高度。

    但是目前已经统一去起来了

    <!DOCTYPE html>复制代码

    CSS3提供了可以切换盒子模型模式的属性:box-sizing,它有2个值,分别是content-box和border-box

    content-box:

    让元素维持W3C的标准盒模型。元素的宽度/高度由border + padding + content的宽度/高度决定,设置width/height属性指的是content部分的宽/高 

    border-box:

    让元素维持IE传统盒模型(IE6以下版本和IE6~7的怪异模式)。设置width/height属性指的是border + padding + content 


    在浏览器输入 URL 回车之后发生了什么

    参考 ruoduan.top/2019/04/11/…

    当我们在web浏览器的地址栏中输入: www.baidu.com,然后回车,到底发生了什么

    1.对www.baidu.com这个网址进行DNS域名解析,得到对应的IP地址   

    2.根据这个IP,找到对应的服务器,发起TCP的三次握手   

    3.建立TCP连接后发起HTTP请求   

    4.服务器响应HTTP请求,浏览器得到html代码   

    5.浏览器解析html代码,并请求html代码中的资源(如js、css图片等)(先得到html代码,才能去找这些资源)   

    6.浏览器对页面进行渲染呈现给用户

    三个主要过程:

    • DNS 解析
    • TCP 连接
    • HTTP 请求/响应

    最后一步对前端很重要 浏览器是如何对页面进行渲染的?

    a: 解析html文件构成 DOM树, 

    b: 解析CSS文件构成渲染树, 

    c: 边解析,边渲染 ,  

    d: JS 单线程运行,JS有可能修改DOM结构,意味着JS执行完成前,后续所有资源的下载是没有必要的,所以JS是单线程,会阻塞后续资源下载

    new操作符具体做了什么

    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.run = function() {
        console.log(this.name + 'can run...');
    }
    
    var cat = new Animal('cat');
    cat.run() //catcan run...
    console.log(cat.__proto__ === Animal.prototype); // true复制代码

    new共经历了四个过程。

    var fn = function () { };
    var fnObj = new fn();复制代码

    1、创建了一个空对象

    var obj = new object();复制代码

    2、设置原型链

    obj._proto_ = fn.prototype;复制代码

    3、让fn的this指向obj,并执行fn的函数体

    var result = fn.call(obj);复制代码

    4、判断fn的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

    if (typeof(result) == "object"){  
        fnObj = result;  
    } else {  
        fnObj = obj;
    }  复制代码


    Symbol,Map和Set

    参考 blog.csdn.net/xzz2222/art…

    为啥需要Symbol:

    一个新规则的提出,必然是因为有需求,熟悉ES5的人都知道,ES5里面对象的属性名都是字符串,如果你需要使用一个别人提供的对象,你对这个对象有哪些属性也不是很清楚,但又想为这个对象新增一些属性,那么你新增的属性名就很可能和原来的属性名发送冲突,显然我们是不希望这种情况发生的。所以,我们需要确保每个属性名都是独一无二的,这样就可以防止属性名的冲突了。因此,ES6里就引入了Symbol,用它来产生一个独一无二的值。

    Symbol是什么:

    Symbol实际上是ES6引入的一种原始数据类型,除了Symbol,JavaScript还有其他5种数据类型,分别是Undefined、Null、Boolean、String、Number,这5种数据类型都是ES5中就有的。

    怎么生成一个Symbol类型的值:

    既然我们已经知道了Symbol是一种原始的数据类型,那么怎么生成这种数据类型的值呢?Symbol值是通过Symbol函数生成的,如下:

    let s = Symbol();
    console.log(s);  // Symbol()
    typeof s;  // "symbol"复制代码

    上面代码中,s就是一个Symbol类型的值,它是独一无二的。

    Symbol函数前不能用new

    Symbol函数不是一个构造函数,前面不能用new操作符。所以Symbol类型的值也不是一个对象,不能添加任何属性,它只是一个类似于字符型的数据类型。如果强行在Symbol函数前加上new操作符,会报错,如下:

    let s = new Symbol();// Uncaught TypeError: Symbol is not a constructor(…)复制代码

    Symbol函数的参数

    1.字符串作为参数

    用上面的方法生成的Symbol值不好进行区分,Symbol函数还可以接受一个字符串参数,来对产生的Symbol值进行描述,方便我们区分不同的Symbol值。

    let s1 = Symbol('s1');
    let s2 = Symbol('s2');
    console.log(s1);  // Symbol(s1)
    console.log(s2);  // Symbol(s2)
    s1 === s2;  //  false
    let s3 = Symbol('s2');
    s2 === s3;  //  false
    复制代码

    从上面代码可以看出:

    1. 给Symbol函数加了参数之后,控制台输出的时候可以区分到底是哪一个值;
    2. Symbol函数的参数只是对当前Symbol值的描述,因此相同参数的Symbol函数返回值是不相等的;

    2.对象作为参数

    如果Symbol函数的参数是一个对象,就会调用该对象的toString方法,将其转化为一个字符串,然后才生成一个Symbol值。所以,说到底,Symbol函数的参数只能是字符串。

    set数据集合:类似于数组 

    set:是一个集合,类似于数组,与数组的主要区别是没有重复的元素。主要的作用可以进行去重。 

    重点:一个属性,四个方法 

    1、size属性:返回set数组的长度,类似于lenght 

    2、四个方法:add,delete,clear,has 

    let set =new Set([1,2,3,4,1]);
    console.log(set.size); //4
    //添加元素
    set.add(7);
    console.log(set); // {1, 2, 3, 4, 7}
    //删除数组中的某个元素
    set.delete(3);
    console.log(set); // {1, 2, 4, 7}
    //检测数组中是否含有某个元素,返回值为布尔
    console.log(set.has(2)); //true
    //清空数组
    set.clear();
    console.log(set) //{}复制代码

    map数据集合:类似于对象 

    let obj={a:1};
    const map = new Map([
        ['name','java'],
        ['feel','今天天气贼好'],
        ['jieguo','适合看帅哥'],
        [obj,'是!']
    ]);
    console.log(map);//{"name" => "java", "feel" => "今天天气贼好", "jieguo" => "适合看帅哥", {…} => "是!"}复制代码

    Map与Object的区别 

     1、Map的size属性可以直接获取键值对个数 

     2、两者都是对应的keys-value键值对, 但是Object的keys只能是字符串或者 Symbols, Map的key可以是任意值,比如数组、对象等  


    总结:Map 对象保存键值对。一个对象的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值。 Set 对象允许你存储任何类型的唯一值,Set对象是值的集合,Set中的元素只会出现一次 Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用(Symbol([description]) )


    Vue 数组处理

    <template>
      <div>
        {{list[2]}}
      </div>
    </template>
    <script>
        export default {
            data() {
                return {
                    list: [1,3,5]
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {
                this.list[2] = 8 // 没有效果
                this.$set(this.list, 2, 8); //有效果
            }
        }
    </script>复制代码

    对于set这个方法的解释: 

    this.$set(数组或者对象,修改的下标或者对象属性名,修改的值)

    JS数组常用算法详解

    1、不改变原数组,返回新数组

    1、concat() 连接两个或多个数组,两边的原始数组都不会变化,返回被连接数组的一个副本。  

    2、join() 把数组中所有元素放入一个字符串中,返回字符串。 

    3、slice() 从开始到结束(不包括结束)选择数组的一部分浅拷贝到一个新数组。

    4、map() 创建一个新数组并返回,其中新数组的每个元素由调用原始数组中的每一个元素执行提供的函数得来,原始数组不会改变。 

    5、every() 对数组中的每个元素都执行一次指定的回调函数,直到回调函数返回false,此时every()返回false并不再继续执行。如果回调函数对每个元素都返回true,那么every()将返回true。

    6、some() 对数组中的每个元素都执行一次指定的回调函数,直到回调函数返回true,此时some()返回true并不再继续执行。如果回调函数对每个元素都返回false,那么some()将返回false。 

    7、filter() 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

    2、改变原数组

    1、forEach() 针对每一个元素执行提供的函数。会修改原来的数组,不会返回执行结果,返回undefined。 

    2、pop() 删除数组最后一个元素,返回被删除的元素。如果数组为空,则不改变数组,返回undefined。 

    3、push() 向数组末尾添加一个或多个元素,返回改变后数组的长度。 reverse() 颠倒数组中元素的位置,返回该数组的引用。 

    4、shift() 从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。 

    5、unshift() 将一个或多个元素添加到数组的开头,并返回新数组的长度。 

    6、sort() 对数组的元素进行排序,并返回数组。排序不一定是稳定的。默认排序顺序是根据字符串Unicode码点。 

    7、splice() 向数组中添加/删除项目,然后返回被删除项目的新数组

    GET,POST,PUT,Delete

    GET请求会向数据库获取信息,只是用来查询数据,不会修改,增加数据。使用URL传递参数,对所发送的数量有限制,一般在2000字符


    POST向服务器发送数据,会改变数据的种类等资源,就像insert操作一样,会创建新的内容,大小一般没有限制,POST安全性高,POST不会被缓存


    PUT请求就像数据库的update操作一样,用来修改数据内容,不会增加数据种类


    Delete用来删除操作

    GET和POST的区别:

    1.GET使用URL或Cookie传参,而POST将数据放在BODY中,这个是因为HTTP协议用法的约定。并非它们的本身区别。
    2.GET方式提交的数据有长度限制,则POST的数据则可以非常大,这个是因为它们使用的操作系统和浏览器设置的不同引起的区别。也不是GET和POST本身的区别。
    3.POST比GET安全,因为数据在地址栏上不可见,这个说法没毛病,但依然不是GET和POST本身的区别。

    4.GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是。(幂等性:对同一URL的多个请求应该返回同样的结果。)因为get请求是幂等的,在网络不好的隧道中会尝试重试。如果用get请求增数据,会有重复操作的风险,而这种重复操作可能会导致副作用


    vue router的钩子函数

    1、全局的钩子

    beforeEach(to,from,next)(全局前置守卫)  页面加载之前  

    • to:即将要进入的目标
    • from:当前导航正要离开的路由
    • next:function函数,必须调用
    应用场景:
    进行页面的跳转,如判断登录页面是否需要进行拦截

    afterEach(to,from,next)(全局后置守卫) 页面加载之后,有两个参数:to/from ,全局后置钩子在所有路由跳转结束的时候调用这些钩子不会接受 next 函数也不会改变导航本身

    vue router.beforeResolve(全局解析守卫)


    注:beforeEach和afterEach都是vue-router实例对象的属性,每次跳转前,beforeEach和afterEach都会执行。

    2、组建内的导航钩子: 

    beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave,直接在路由组件内部进行定义的  

    beforeRouteEnter(to, from, next) :

    在渲染该组件的对应路由被确认前调用,用法和参数与beforeEach类似,next需要被主动调用
    注意:

    • 此时组件实例还未被创建,不能访问this
    • 可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
    beforeRouteEnter (to, from, next) {
      // 这里还无法访问到组件实例,this === undefined
      next( vm => {
        // 通过 `vm` 访问组件实例
      })
    }复制代码
    • 可以在这个守卫中请求服务端获取数据,当成功获取并能进入路由时,调用next并在回调中通过 vm访问组件实例进行赋值等操作
    • beforeRouteEnter触发在导航确认、组件实例创建之前:beforeCreate之前;而next中函数的调用在mounted之后:为了确保能对组件实例的完整访问

    beforeRouteUpdate (to, from, next) :

    在当前路由改变,并且该组件被复用时调用,可以通过this访问实例, next需要被主动调用,不能传回调

    • 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,组件实例会被复用,该守卫会被调用
    • 当前路由query变更时,该守卫会被调用

    beforeRouteLeave (to, from, next) :

    航离开该组件的对应路由时调用,可以访问组件实例 thisnext需要被主动调用,不能传回调 用途:清除当前组件中的定时器,避免占用内存;当页面中有未关闭的窗口, 或未保存的内容时, 阻止页面跳转;保存相关内容到Vuex中或Session中 

    3、路由内的导航钩子

    主要用于写某个指定路由跳转时需要执行的逻辑

    beforeEnter

    有三个参数:to/from/next

    {
        path:'/',
        name:'Login',
        component:Login,
        beforeEnter:(to,from,next)=>{
            consloe.log("即将进入Login");
            next();
        }
    }复制代码



    关注下面的标签,发现更多相似文章
    评论