九是我呀

九是我呀 查看完整档案

苏州编辑西安交通大学  |  软件工程 编辑  |  填写所在公司/组织 www.jianshu.com/u/09800922c188 编辑
编辑

希望每写一篇优质文章,工资就涨100元。

个人动态

九是我呀 发布了文章 · 1月26日

箭头函数理解

参见 https://www.jianshu.com/p/051...
https://segmentfault.com/a/11...

接下来仨
https://segmentfault.com/a/11...
箭头函数
https://es6.ruanyifeng.com/#d...
https://segmentfault.com/a/11...

以下主要记录自己不太懂的地方,详解见上述链接

注意点

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

箭头函数根本没有自己的this,导致内部的this指向了外层代码的this这个指向在定义时就已经确定而不会在调用时指向其执行环境的(变量)对象。除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。
image

基础
  1. 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
let getTempItem = id => ({ id: id, name: "Temp" });
  1. 箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}
用途
  1. 简化回调函数
  2. 解构为数组
const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
实战

超时调用(setTimeout回调)的代码都是在全局作用域环境中执行的,因此(setTimeout回调)函数中this的值在非严格模式下指向window对象,在严格模式下是undefined

不适用场合
第一个场合是定义对象的方法,且该方法内部包括this。会指向全局变量,因为对象不构成单独的作用域,导致箭头函数定义时的作用域就是全局作用域。

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

第二个场合是需要动态this的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

嵌套的箭头函数(没看)

查看原文

赞 0 收藏 0 评论 0

九是我呀 发布了文章 · 1月26日

再也不怕面试中this的指向问题

参考

【第1318期】深入浅出 JavaScript 关键词 -- this
https://juejin.cn/post/684490...

写这篇文章原因

因为总结面经中遇到了this的指向问题,这也是面试官常问的基础问题。所以单独列出来供参考学习

this指向几点判断规则

js的函数除了声明定义的形参之外,每个函数还接受两个附加参数:this和arguments。this 的值并不是由函数定义放在哪个对象里面决定,而是函数执行时由谁来唤起决定。

  1. 函数调用模式:strict情况下,指向undefined,否则指向全局变量
  2. 方法调用模式:指向方法所在的对象
  3. 构造函数模式调用,用new时,this指向新构建的对象
  4. apply,call明确绑定: 或者bind绑定,this指向被绑定的对象
  5. 箭头函数内部的this就是外层代码块的this,包裹在函数中时this指向函数调用时所在的对象,若放在全局中则是指向全局对象window,且不会改变
// Q1 规则1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window
var a = 1;
function print () {
 console.log(this.a)
}            
print()  //a=1

// Q2 规则2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
const obj = {
 a: 2,
 print: function () { console.log(this.a) }
}
obj.print();  //2
​
// Q3 
const obj = {
 a: 3,
 print: function () { console.log(this.a) }
}
obj.print()  //3  与下面的foo()区别在于是全局环境还是obj环境
const foo = obj.print; 
foo()     //undefined      全局情况下调用的,没有全局变量a

// Q3.1   如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
const obj = {
    a: 3,
    printOut:{
        a:4,
        printIn: function(){ console.log(this.a)},
    },
}
obj.printOut.printIn()    //4 
const foo = obj.printOut.printIn;  
foo()     //undefined      全局情况下调用的,没有全局变量a
​
// Q4  Q4-Q9参见规则5
const obj = {
 a: 4,
 print: () => { console.log(this.a) }
}
obj.print();                 //undefined     有箭头函数,对象不构成单独的作用域,导致`print`箭头函数定义时的作用域就是全局作用域。
​
// Q5
var a = 5
const obj = {
 a: 6,
 print: () => { console.log(this.a) }
}
obj.print.call({a: 7}); //5         理由同上
​
// Q6 
function Person () {
 this.a = 8
 this.print = function () {console.log(this.a)}
 return {a: 9} 
}
const p = new Person()  
console.log(p.a)  //9   此时p为构造函数返回的{a:9}
console.log(p.print())  // undefined p是没有print函数的
​
// Q7 
'use strict';
var a = 1;
​
function print () {
 console.log(this.a)
}
print()    //undefined 严格模式下,this指向undefined

// Q8 普通函数
function foo() {
  setTimeout(function() {
    console.log('id:', this.id);
  });
}
var id = 21;
foo.call({ id: 42 }); //21

//Q9 箭头函数,匿名函数定义时所在的执行环境就是foo函数,所以匿名函数内部的this执向始终会和foo函数的this执向保持一致,指向call的对象。
为什么指向call的对象呢?参见call的用法:函数名.call(对象,arg1,arg2,...argn),功能是调用函数,并把函数内部的this指向第一个参数的对象,然后将第二个及之后的是所有的参数,作为实参传给函数。
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;
foo.call({ id: 42 }); //42

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;
foo(); //21(没有用call)

// Q10
var A = function( name ){ 
    this.name = name;
};
var B = function(){ 
    A.apply(this,arguments);
};
B.prototype.getName = function(){ 
    return this.name;
};
var b=new B('sven');
console.log( b.getName() ); // 输出:  'sven'
查看原文

赞 0 收藏 0 评论 0

九是我呀 发布了文章 · 1月14日

几种常见的排序方法(JS实现)

前言

就几种常见的排序算法用js语言实现: 冒泡排序,选择排序,插入排序,希尔排序,快速排序,归并排序,桶排序。

尚未完成
1.分析排序算法:基本思想,稳定性,空间和时间复杂度
2.用图片、视频算法,深入浅出

排序总结
image

下面就来进入主题吧

冒泡排序

思想:每次都在未排序的序列中找到最大(小)元素,放在末尾:方法是与相邻元素比较,决定是否交换。
时间复杂度:O(nn) ,因为两个for循环:O(n)O(n)

var  bubbleSort = function(arr){
    for(let i=0; i < arr.length-1; i++){ //i表示趟数   
      for(let j=0; j < arr.length-1-i; j++){ //还未排序的数量
        if(arr[j+1] < arr[j]){  //与相邻元素比较
          [arr[j],arr[j+1]] = [arr[j+1], arr[j]]      // ES新特性
        }
      }
    }
    return arr
  }
选择排序

思想:从未排序的序列中找到最大(小)元素放在排序序列的末尾,作为已排序的一员。然后接着找当前未排序元素的最大(小)

var selectSort = function (arr) {
    for(let i = 0; i < arr.length-1; i++){ //趟数
        let min  = arr[i]
        let curIndex = i    //  保存要交换位置的索引值
        for( let j = i; j < arr.length ; j++ ){   
            if( arr[j] < min ){
                min = arr[j]
                curIndex = j
            }
        }
       [arr[curIndex], arr[i]] = [arr[i], arr[curIndex]] //交换
    }
    return arr
}
插入排序

思想:分为两组:左边为有序,右边为无序,在右边序列里每找到一个,跟左边已排序部分倒序地进行比较,把它插入到前面已排序的组中

var insertSort = function(arr){
    var preIndex =0  //当前访问的元素的前一个索引
    for(var i=1; i<arr.length; i++){
        var temp = arr[i]
        preIndex = i-1 
        while(preIndex >= 0 && temp < arr[preIndex] ) { //若当前值小于前面的值,则交换,继续向前比较。这时候不能再用arr[i],应该用temp
            arr[preIndex+1] = arr[preIndex]  
            preIndex -= 1
        }
        arr[preIndex+1] = temp  //找到了要插入的位置
    }
    return arr;
}
希尔排序

思想:是变形的插入排序,利用插入排序在基本有序的情况下时间复杂度低。

var shellSort = function(arr){
    var len =arr.length, temp, gap =1;
    while(gap< len/3){
        gap=gap*3  +  1
    }  //确定gap,根据arr的长度
    for( gap; gap>0; gap =Math.floor(gap/3)){
        /**
         * 可以替换掉下面这个for循环
         *  for(var i= gap; i<len; i++){
         *      temp = arr[i]
         *      for(var preIndex = i-gap ;preIndex >=0 && arr[preIndex]> temp ; preIndex -=gap){
         *          arr[preIndex + gap]= arr[preIndex]
         *      }
         *      arr[preIndex +gap]= temp
         *  }
          */
        for(var i =gap; i<len; i++){
            temp = arr[i]
            preIndex =i -gap
            while(preIndex >=0 && arr[preIndex]> temp){
                arr[preIndex + gap]= arr[preIndex]
                preIndex -= gap
            }
            arr[preIndex +gap]= temp
        }
    }
    return arr
}
快排
//令人头疼,知道思路,但是写起来确实有点费事,这个方式跟阮一峰老师的有点差别,这是就地排序,没有占用其他空间。
//递归算法
var quickSort = function (arr, left, right ) {
    let target = arr[left]
    let i =left ,j =right;  //
    if(left > right){  //特别注意这点
        return;
    }
    while( i !== j){
        while(arr[j]>= target && i<j){
            j--
        }
        while(arr[i]<= target && i<j){
            i++
        }
        if(i<j){
            [arr[i],arr[j]] = [arr[j],arr[i]]
        }
    }
    arr[left] =arr[i]
    arr[i]= target
    // 这里的left和下面的right
    quickSort(arr,left,i-1)  
    quickSort(arr,i+1,right)
    return arr
}
归并排序

思想:先将数组拆分成几个小的组,然后合在一起

var merge = function(left, right) {  //合并两个有序数组
    var result = [];
    while(left.length && right.length){
        if(left[0] > right[0]){ //每次选取left和right数组中最小的元素进入result
            result.push(right.shift()) //做了两个操作1.删除right中最小的元素 2.将最小的元素放入result中
        } else{
            result.push(left,shift())
        }
    }
    return result.concat(right,left)  //将left或者right中剩下的元素一起放入result中
}

var mergeSort = function(arr){
    //判断长度
    if(arr.length<=1){
        return arr
    }
    var mid = Math.floor(arr.length/2)
    var left = arr.slice(0,mid)
    var right = arr.slice(mid)
    return merge(mergeSort(left),mergeSort(right))
}
桶排序

思想:如有10个数字在1-100以内,设置10个桶,每个桶所能容纳的范围分别为1-10,11-20,...... ,91-100。
将这10个数字放在相应的桶中,放入同一个桶时采用插入排序,这样保证桶内也是有序的。整体桶也是有序,直接合并就行

var bucketSort1 = function (arr) {
    var min = arr[0]
    var max = arr[0]
    var len = arr.length;
    var sum = len;
    var size = 0
    var index;
    for (var i = 0; i < len; i++) {
        if (arr[i] > max) {
            max = arr[i]
        } else if (arr[i] < min) {
            min = arr[i]
        }
    }
    // 边界条件判断 : min是否等于max,若等于,则无需排序
    if (max != min) {
        var bucket = new Array(len)
        for (var i = 0; i < sum; i++) {  //把数组中的数分到各个桶中
            index = Math.floor((arr[i] - min) / (max - min) * (sum - 1))
            bucket[index] = new Array()  //这句定义数组。不能省略
            bucket[index].push(arr[i])  //这个地方可以换位插入排序或者其他排序
        }
        for (var i = 0; i < sum; i++) {
            if (bucket[i].length > 1) {
                bucketSort1(bucket[i])
            }
            for (var j = 0; j < bucket[i].length; j++) {
                arr[size++] = bucket[i][j]
            }
        }
    }
    return arr
}
参考

各种排序算法总结:https://www.jianshu.com/p/f5b...

查看原文

赞 0 收藏 0 评论 0

九是我呀 收藏了文章 · 1月11日

前端面试js高频手写大全

介绍

在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。

一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。

编程题主要分为这几种类型:

* 算法题
* 涉及js原理的题以及ajax请求
* 业务场景题: 实现一个具有某种功能的组件
* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等

其中前两种类型所占比重最大。
算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS

本文主要涵盖了第二种类型的各种重点手写。

建议优先掌握

  • instanceof (考察对原型链的理解)
  • new (对创建对象实例过程的理解)
  • call&apply&bind (对this指向的理解)
  • 手写promise (对异步的理解)
  • 手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)
  • 其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)

话不多说,直接开始

1. 手写instanceof

instanceof作用:

判断一个实例是否是其父类或者祖先类型的实例。

instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

 let myInstanceof = (target,origin) => {
     while(target) {
         if(target.__proto__===origin.prototype) {
            return true
         }
         target = target.__proto__
     }
     return false
 }
 let a = [1,2,3]
 console.log(myInstanceof(a,Array));  // true
 console.log(myInstanceof(a,Object));  // true

2. 实现数组的map方法

数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

用法:

const a = [1, 2, 3, 4];
const b = array1.map(x => x * 2);
console.log(b);   // Array [2, 4, 6, 8]

原生实现:

 Array.prototype.myMap = function(fn, thisValue) {
     let res = []
     thisValue = thisValue||[]
     let arr = this
     for(let i in arr) {
        res.push(fn(arr[i]))
     }
     return res
 }

3. reduce实现数组的map方法

利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

Array.prototype.myMap = function(fn,thisValue){
     var res = [];
     thisValue = thisValue||[];
     this.reduce(function(pre,cur,index,arr){
         return res.push(fn.call(thisValue,cur,index,arr));
     },[]);
     return res;
}
​
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
 console.log(item,index,arr);
})

4. 手写数组的reduce方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

参数:

  • callback(一个在数组中每一项上调用的函数,接受四个函数:)

    • previousValue(上一次调用回调函数时的返回值,或者初始值)
    • currentValue(当前正在处理的数组元素)
    • currentIndex(当前正在处理的数组元素下标)
    • array(调用reduce()方法的数组)
  • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
 function reduce(arr, cb, initialValue){
     var num = initValue == undefined? num = arr[0]: initValue;
     var i = initValue == undefined? 1: 0
     for (i; i< arr.length; i++){
        num = cb(num,arr[i],i)
     }
     return num
 }
 
 function fn(result, currentValue, index){
     return result + currentValue
 }
 
 var arr = [2,3,4,5]
 var b = reduce(arr, fn,10) 
 var c = reduce(arr, fn)
 console.log(b)   // 24

5. 数组扁平化

数组扁平化就是把多维数组转化成一维数组

1. es6提供的新方法 flat(depth)

let a = [1,[2,3]]; 
a.flat(); // [1,2,3] 
a.flat(1); //[1,2,3]

其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

let a = [1,[2,3,[4,[5]]]]; 
a.flat(Infinity); // [1,2,3,4,5]  a是4维数组

2. 利用cancat

function flatten(arr) {
     var res = [];
     for (let i = 0, length = arr.length; i < length; i++) {
     if (Array.isArray(arr[i])) {
     res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
     //res.push(...flatten(arr[i])); //或者用扩展运算符 
     } else {
         res.push(arr[i]);
       }
     }
     return res;
 }
 let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

6. 函数柯里化

柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

有两种思路:

  1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  2. 在调用柯里化工具函数时,手动指定所需的参数个数

将这两点结合一下,实现一个简单 curry 函数:


/**
 * 将函数柯里化
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数,默认为原函数的形参个数
 */
function curry(fn,len = fn.length) {
 return _curry.call(this,fn,len)
}
​
/**
 * 中转函数
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数
 * @param args  已接收的参数列表
 */
function _curry(fn,len,...args) {
    return function (...params) {
         let _args = [...args,...params];
         if(_args.length >= len){
             return fn.apply(this,_args);
         }else{
          return _curry.call(this,fn,len,..._args)
         }
    }
}

我们来验证一下:

let _fn = curry(function(a,b,c,d,e){
 console.log(a,b,c,d,e)
});
​
_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

直接看一下官网的例子:

img

接下来我们来思考,如何实现占位符的功能。

对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

直接上代码:


/**
 * @param  fn           待柯里化的函数
 * @param  length       需要的参数个数,默认为函数的形参个数
 * @param  holder       占位符,默认当前柯里化函数
 * @return {Function}   柯里化后的函数
 */
function curry(fn,length = fn.length,holder = curry){
 return _curry.call(this,fn,length,holder,[],[])
}
/**
 * 中转函数
 * @param fn            柯里化的原函数
 * @param length        原函数需要的参数个数
 * @param holder        接收的占位符
 * @param args          已接收的参数列表
 * @param holders       已接收的占位符位置列表
 * @return {Function}   继续柯里化的函数 或 最终结果
 */
function _curry(fn,length,holder,args,holders){
 return function(..._args){
 //将参数复制一份,避免多次操作同一函数导致参数混乱
 let params = args.slice();
 //将占位符位置列表复制一份,新增加的占位符增加至此
 let _holders = holders.slice();
 //循环入参,追加参数 或 替换占位符
 _args.forEach((arg,i)=>{
 //真实参数 之前存在占位符 将占位符替换为真实参数
 if (arg !== holder && holders.length) {
     let index = holders.shift();
     _holders.splice(_holders.indexOf(index),1);
     params[index] = arg;
 }
 //真实参数 之前不存在占位符 将参数追加到参数列表中
 else if(arg !== holder && !holders.length){
     params.push(arg);
 }
 //传入的是占位符,之前不存在占位符 记录占位符的位置
 else if(arg === holder && !holders.length){
     params.push(arg);
     _holders.push(params.length - 1);
 }
 //传入的是占位符,之前存在占位符 删除原占位符位置
 else if(arg === holder && holders.length){
    holders.shift();
 }
 });
 // params 中前 length 条记录中不包含占位符,执行函数
 if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
 return fn.apply(this,params);
 }else{
 return _curry.call(this,fn,length,holder,params,_holders)
 }
 }
}

验证一下:;

let fn = function(a, b, c, d, e) {
 console.log([a, b, c, d, e]);
}
​
let _ = {}; // 定义占位符
let _fn = curry(fn,5,_);  // 将函数柯里化,指定所需的参数个数,指定所需的占位符
​
_fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1);              // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2);              // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5);        // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5);        // print: 1,2,3,4,5

至此,我们已经完整实现了一个 curry 函数~~

7. 实现深拷贝

浅拷贝和深拷贝的区别:

浅拷贝:只拷贝一层,更深层的对象级别的只拷贝引用

深拷贝:拷贝多层,每一级别的数据都会拷贝。这样更改拷贝值就不影响另外的对象

ES6浅拷贝方法:Object.assign(target,...sources)


let obj={
 id:1,
 name:'Tom',
 msg:{
 age:18
 }
}
let o={}
//实现深拷贝  递归    可以用于生命游戏那个题对二维数组的拷贝,
//但比较麻烦,因为已知元素都是值,直接复制就行,无需判断
function deepCopy(newObj,oldObj){
     for(var k in oldObj){
         let item=oldObj[k]
         //判断是数组?对象?简单类型?
         if(item instanceof Array){
             newObj[k]=[]
             deepCopy(newObj[k],item)
         }else if(item instanceof Object){
             newObj[k]={}
             deepCopy(newObj[k],item)
         }else{  //简单数据类型,直接赋值
             newObj[k]=item
         }
     }
}

8. 手写call, apply, bind

手写call

Function.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上
 if(typeof this !=="function"){   // 这里if其实没必要,会自动抛出错误
    throw new Error("不是函数")
 }
 const obj=context||window   //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
 obj.fn=this      //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
 const arg=[...arguments].slice(1)   //第一个为obj所以删除,伪数组转为数组
 res=obj.fn(...arg)
 delete obj.fn   // 不删除会导致context属性越来越多
 return res
}
//用法:f.call(obj,arg1)
function f(a,b){
 console.log(a+b)
 console.log(this.name)
}
let obj={
 name:1
}
f.myCall(obj,1,2) //否则this指向window

obj.greet.call({name: 'Spike'}) //打出来的是 Spike

手写apply(arguments[this, [参数1,参数2.....] ])

Function.prototype.myApply=function(context){  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
 let obj=context||window
 obj.fn=this
 const arg=arguments[1]||[]    //若有参数,得到的是数组
 let res=obj.fn(...arg)
 delete obj.fn
 return res
} 
function f(a,b){
 console.log(a,b)
 console.log(this.name)
}
let obj={
 name:'张三'
}
f.myApply(obj,[1,2])  //arguments[1]

手写bind

this.value = 2
var foo = {
 value: 1
};
var bar = function(name, age, school){
 console.log(name) // 'An'
 console.log(age) // 22
 console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

简单版本

Function.prototype.bind = function(context, ...outerArgs) {
 var fn = this;
 return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
 return fn.apply(context,[...outerArgs, ...innerArgs]);  //返回改变了this的函数,
 //参数合并
 }
}

new失败的原因:

例:

// 声明一个上下文
let thovino = {
 name: 'thovino'
}
​
// 声明一个构造函数
let eat = function (food) {
 this.food = food
 console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
 console.log('func name : eat')
}
​
// bind一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange')  //实际上orange放到了thovino里面
console.log('instance:', instance) // {}

生成的实例是个空对象

new操作符执行时,我们的thovinoEat函数可以看作是这样:

function thovinoEat (...innerArgs) {
 eat.call(thovino, ...outerArgs, ...innerArgs)
}

在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

可new可继承版本

Function.prototype.bind = function (context, ...outerArgs) {
 let that = this;
​
function res (...innerArgs) {
     if (this instanceof res) {
         // new操作符执行时
         // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
         that.call(this, ...outerArgs, ...innerArgs)
     } else {
         // 普通bind
         that.call(context, ...outerArgs, ...innerArgs)
     }
     }
     res.prototype = this.prototype //!!!
     return res
}

9. 手动实现new

new的过程文字描述:

  1. 创建一个空对象 obj;
  2. 将空对象的隐式原型(proto)指向构造函数的prototype。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.sayHi=function(){
 console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)
​
////手动实现new
function create(){
 let obj={}
 //获取构造函数
 let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象    !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果      或者let arg = [].slice.call(arguments,1)
 obj.__proto__=fn.prototype
 let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
 //确保返回的是一个对象(万一fn不是构造函数)
 return typeof res==='object'?res:obj
}
​
let p2=create(Person,'李四',19)
p2.sayHi()

细节:

[].shift.call(arguments)  也可写成:
 let arg=[...arguments]
 let fn=arg.shift()  //使得arguments能调用数组方法,第一个参数为构造函数
 obj.__proto__=fn.prototype
 //改变this指向,为实例添加方法和属性
 let res=fn.apply(obj,arg)

10. 手写promise(常见promise.all, promise.race)

// Promise/A+ 规范规定的三种状态
const STATUS = {
 PENDING: 'pending',
 FULFILLED: 'fulfilled',
 REJECTED: 'rejected'
}
​
class MyPromise {
 // 构造函数接收一个执行回调
 constructor(executor) {
     this._status = STATUS.PENDING // Promise初始状态
     this._value = undefined // then回调的值
     this._resolveQueue = [] // resolve时触发的成功队列
     this._rejectQueue = [] // reject时触发的失败队列
    ​
 // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
 const resolve = value => {
     const run = () => {
         // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
         if (this._status === STATUS.PENDING) {
             this._status = STATUS.FULFILLED // 更改状态
             this._value = value // 储存当前值,用于then回调
            ​
             // 执行resolve回调
             while (this._resolveQueue.length) {
                 const callback = this._resolveQueue.shift()
                 callback(value)
             }
         }
     }
     //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
     setTimeout(run)
 }
​
 // 同 resolve
 const reject = value => {
     const run = () => {
         if (this._status === STATUS.PENDING) {
         this._status = STATUS.REJECTED
         this._value = value
        ​
         while (this._rejectQueue.length) {
             const callback = this._rejectQueue.shift()
             callback(value)
         }
     }
 }
     setTimeout(run)
 }

     // new Promise()时立即执行executor,并传入resolve和reject
     executor(resolve, reject)
 }
​
 // then方法,接收一个成功的回调和一个失败的回调
 function then(onFulfilled, onRejected) {
  // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
  typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
  typeof onRejected !== 'function' ? onRejected = error => error : null

  // then 返回一个新的promise
  return new MyPromise((resolve, reject) => {
    const resolveFn = value => {
      try {
        const x = onFulfilled(value)
        // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
  }
}
​
  const rejectFn = error => {
      try {
        const x = onRejected(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }

    switch (this._status) {
      case STATUS.PENDING:
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
        break;
      case STATUS.FULFILLED:
        resolveFn(this._value)
        break;
      case STATUS.REJECTED:
        rejectFn(this._value)
        break;
    }
 })
 }
 catch (rejectFn) {
  return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
  return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
    MyPromise.resolve(callback()).then(() => error)
  })
}

 // 静态resolve方法
 static resolve(value) {
      return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
  }

 // 静态reject方法
 static reject(error) {
      return new MyPromise((resolve, reject) => reject(error))
    }

 // 静态all方法
 static all(promiseArr) {
      let count = 0
      let result = []
      return new MyPromise((resolve, reject) =>       {
        if (!promiseArr.length) {
          return resolve(result)
        }
        promiseArr.forEach((p, i) => {
          MyPromise.resolve(p).then(value => {
            count++
            result[i] = value
            if (count === promiseArr.length) {
              resolve(result)
            }
          }, error => {
            reject(error)
          })
        })
      })
    }

 // 静态race方法
 static race(promiseArr) {
      return new MyPromise((resolve, reject) => {
        promiseArr.forEach(p => {
          MyPromise.resolve(p).then(value => {
            resolve(value)
          }, error => {
            reject(error)
          })
        })
      })
    }
}

11. 手写原生AJAX

步骤

  1. 创建 XMLHttpRequest 实例
  2. 发出 HTTP 请求
  3. 服务器返回 XML 格式的字符串
  4. JS 解析 XML,并更新局部页面

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON

了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

version 1.0:

myButton.addEventListener('click', function () {
  ajax()
})

function ajax() {
  let xhr = new XMLHttpRequest() //实例化,以调用方法
  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步
  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。
    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。
      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功
        let string = request.responseText
        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
        let object = JSON.parse(string)
      }
    }
  }
  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}

promise实现

function ajax(url) {
  const p = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open('get', url)
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status <= 300) {
          resolve(JSON.parse(xhr.responseText))
        } else {
          reject('请求出错')
        }
      }
    }
    xhr.send()  //发送hppt请求
  })
  return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
  .catch(reason => console.log(reason))

12. 手写节流防抖函数

函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

节流:连续触发事件但是在 n 秒中只执行一次函数

例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

防抖的实现:

function debounce(fn, delay) {
     if(typeof fn!=='function') {
        throw new TypeError('fn不是函数')
     }
     let timer; // 维护一个 timer
     return function () {
         var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
         var args = arguments;
         if (timer) {
            clearTimeout(timer);
         }
         timer = setTimeout(function () {
            fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
         }, delay);
     };
}

// 调用​
input1.addEventListener('keyup', debounce(() => {
 console.log(input1.value)
}), 600)

节流的实现:

function throttle(fn, delay) {
  let timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {
      return;
    }
    timer = setTimeout(function () {
      fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
      // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
      timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
    }, delay)
  }
}

div1.addEventListener('drag', throttle((e) => {
  console.log(e.offsetX, e.offsetY)
}, 100))

13. 手写Promise加载图片

function getData(url) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url,
      success(data) {
        resolve(data)
      },
      error(err) {
        reject(err)
      }
    })
  })
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
  console.log(data1)
  return getData(url2)
}).then(data2 => {
  console.log(data2)
  return getData(url3)
}).then(data3 =>
  console.log(data3)
).catch(err =>
  console.error(err)
)

14. 函数实现一秒钟输出一个数

for(let i=0;i<=10;i++){   //用var打印的都是11
 setTimeout(()=>{
    console.log(i);
 },1000*i)
}

15. 创建10个标签,点击的时候弹出来对应的序号?

var a
for(let i=0;i<10;i++){
 a=document.createElement('a')
 a.innerHTML=i+'<br>'
 a.addEventListener('click',function(e){
     console.log(this)  //this为当前点击的<a>
     e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
     //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
     alert(i)
 })
 const d=document.querySelector('div')
 d.appendChild(a)  //append向一个已存在的元素追加该元素。
}

参考:

数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3

函数柯里化 https://juejin.im/post/6844903882208837645

节流防抖 https://www.jianshu.com/p/c8b...

查看原文

九是我呀 赞了文章 · 1月11日

前端面试js高频手写大全

介绍

在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。

一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。

编程题主要分为这几种类型:

* 算法题
* 涉及js原理的题以及ajax请求
* 业务场景题: 实现一个具有某种功能的组件
* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等

其中前两种类型所占比重最大。
算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS

本文主要涵盖了第二种类型的各种重点手写。

建议优先掌握

  • instanceof (考察对原型链的理解)
  • new (对创建对象实例过程的理解)
  • call&apply&bind (对this指向的理解)
  • 手写promise (对异步的理解)
  • 手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)
  • 其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)

话不多说,直接开始

1. 手写instanceof

instanceof作用:

判断一个实例是否是其父类或者祖先类型的实例。

instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

 let myInstanceof = (target,origin) => {
     while(target) {
         if(target.__proto__===origin.prototype) {
            return true
         }
         target = target.__proto__
     }
     return false
 }
 let a = [1,2,3]
 console.log(myInstanceof(a,Array));  // true
 console.log(myInstanceof(a,Object));  // true

2. 实现数组的map方法

数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

用法:

const a = [1, 2, 3, 4];
const b = array1.map(x => x * 2);
console.log(b);   // Array [2, 4, 6, 8]

原生实现:

 Array.prototype.myMap = function(fn, thisValue) {
     let res = []
     thisValue = thisValue||[]
     let arr = this
     for(let i in arr) {
        res.push(fn(arr[i]))
     }
     return res
 }

3. reduce实现数组的map方法

利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

Array.prototype.myMap = function(fn,thisValue){
     var res = [];
     thisValue = thisValue||[];
     this.reduce(function(pre,cur,index,arr){
         return res.push(fn.call(thisValue,cur,index,arr));
     },[]);
     return res;
}
​
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
 console.log(item,index,arr);
})

4. 手写数组的reduce方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

参数:

  • callback(一个在数组中每一项上调用的函数,接受四个函数:)

    • previousValue(上一次调用回调函数时的返回值,或者初始值)
    • currentValue(当前正在处理的数组元素)
    • currentIndex(当前正在处理的数组元素下标)
    • array(调用reduce()方法的数组)
  • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
 function reduce(arr, cb, initialValue){
     var num = initValue == undefined? num = arr[0]: initValue;
     var i = initValue == undefined? 1: 0
     for (i; i< arr.length; i++){
        num = cb(num,arr[i],i)
     }
     return num
 }
 
 function fn(result, currentValue, index){
     return result + currentValue
 }
 
 var arr = [2,3,4,5]
 var b = reduce(arr, fn,10) 
 var c = reduce(arr, fn)
 console.log(b)   // 24

5. 数组扁平化

数组扁平化就是把多维数组转化成一维数组

1. es6提供的新方法 flat(depth)

let a = [1,[2,3]]; 
a.flat(); // [1,2,3] 
a.flat(1); //[1,2,3]

其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

let a = [1,[2,3,[4,[5]]]]; 
a.flat(Infinity); // [1,2,3,4,5]  a是4维数组

2. 利用cancat

function flatten(arr) {
     var res = [];
     for (let i = 0, length = arr.length; i < length; i++) {
     if (Array.isArray(arr[i])) {
     res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
     //res.push(...flatten(arr[i])); //或者用扩展运算符 
     } else {
         res.push(arr[i]);
       }
     }
     return res;
 }
 let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

6. 函数柯里化

柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

有两种思路:

  1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  2. 在调用柯里化工具函数时,手动指定所需的参数个数

将这两点结合一下,实现一个简单 curry 函数:


/**
 * 将函数柯里化
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数,默认为原函数的形参个数
 */
function curry(fn,len = fn.length) {
 return _curry.call(this,fn,len)
}
​
/**
 * 中转函数
 * @param fn    待柯里化的原函数
 * @param len   所需的参数个数
 * @param args  已接收的参数列表
 */
function _curry(fn,len,...args) {
    return function (...params) {
         let _args = [...args,...params];
         if(_args.length >= len){
             return fn.apply(this,_args);
         }else{
          return _curry.call(this,fn,len,..._args)
         }
    }
}

我们来验证一下:

let _fn = curry(function(a,b,c,d,e){
 console.log(a,b,c,d,e)
});
​
_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

直接看一下官网的例子:

img

接下来我们来思考,如何实现占位符的功能。

对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

直接上代码:


/**
 * @param  fn           待柯里化的函数
 * @param  length       需要的参数个数,默认为函数的形参个数
 * @param  holder       占位符,默认当前柯里化函数
 * @return {Function}   柯里化后的函数
 */
function curry(fn,length = fn.length,holder = curry){
 return _curry.call(this,fn,length,holder,[],[])
}
/**
 * 中转函数
 * @param fn            柯里化的原函数
 * @param length        原函数需要的参数个数
 * @param holder        接收的占位符
 * @param args          已接收的参数列表
 * @param holders       已接收的占位符位置列表
 * @return {Function}   继续柯里化的函数 或 最终结果
 */
function _curry(fn,length,holder,args,holders){
 return function(..._args){
 //将参数复制一份,避免多次操作同一函数导致参数混乱
 let params = args.slice();
 //将占位符位置列表复制一份,新增加的占位符增加至此
 let _holders = holders.slice();
 //循环入参,追加参数 或 替换占位符
 _args.forEach((arg,i)=>{
 //真实参数 之前存在占位符 将占位符替换为真实参数
 if (arg !== holder && holders.length) {
     let index = holders.shift();
     _holders.splice(_holders.indexOf(index),1);
     params[index] = arg;
 }
 //真实参数 之前不存在占位符 将参数追加到参数列表中
 else if(arg !== holder && !holders.length){
     params.push(arg);
 }
 //传入的是占位符,之前不存在占位符 记录占位符的位置
 else if(arg === holder && !holders.length){
     params.push(arg);
     _holders.push(params.length - 1);
 }
 //传入的是占位符,之前存在占位符 删除原占位符位置
 else if(arg === holder && holders.length){
    holders.shift();
 }
 });
 // params 中前 length 条记录中不包含占位符,执行函数
 if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
 return fn.apply(this,params);
 }else{
 return _curry.call(this,fn,length,holder,params,_holders)
 }
 }
}

验证一下:;

let fn = function(a, b, c, d, e) {
 console.log([a, b, c, d, e]);
}
​
let _ = {}; // 定义占位符
let _fn = curry(fn,5,_);  // 将函数柯里化,指定所需的参数个数,指定所需的占位符
​
_fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1);              // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2);              // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5);        // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5);        // print: 1,2,3,4,5

至此,我们已经完整实现了一个 curry 函数~~

7. 实现深拷贝

浅拷贝和深拷贝的区别:

浅拷贝:只拷贝一层,更深层的对象级别的只拷贝引用

深拷贝:拷贝多层,每一级别的数据都会拷贝。这样更改拷贝值就不影响另外的对象

ES6浅拷贝方法:Object.assign(target,...sources)


let obj={
 id:1,
 name:'Tom',
 msg:{
 age:18
 }
}
let o={}
//实现深拷贝  递归    可以用于生命游戏那个题对二维数组的拷贝,
//但比较麻烦,因为已知元素都是值,直接复制就行,无需判断
function deepCopy(newObj,oldObj){
     for(var k in oldObj){
         let item=oldObj[k]
         //判断是数组?对象?简单类型?
         if(item instanceof Array){
             newObj[k]=[]
             deepCopy(newObj[k],item)
         }else if(item instanceof Object){
             newObj[k]={}
             deepCopy(newObj[k],item)
         }else{  //简单数据类型,直接赋值
             newObj[k]=item
         }
     }
}

8. 手写call, apply, bind

手写call

Function.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上
 if(typeof this !=="function"){   // 这里if其实没必要,会自动抛出错误
    throw new Error("不是函数")
 }
 const obj=context||window   //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
 obj.fn=this      //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
 const arg=[...arguments].slice(1)   //第一个为obj所以删除,伪数组转为数组
 res=obj.fn(...arg)
 delete obj.fn   // 不删除会导致context属性越来越多
 return res
}
//用法:f.call(obj,arg1)
function f(a,b){
 console.log(a+b)
 console.log(this.name)
}
let obj={
 name:1
}
f.myCall(obj,1,2) //否则this指向window

obj.greet.call({name: 'Spike'}) //打出来的是 Spike

手写apply(arguments[this, [参数1,参数2.....] ])

Function.prototype.myApply=function(context){  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
 let obj=context||window
 obj.fn=this
 const arg=arguments[1]||[]    //若有参数,得到的是数组
 let res=obj.fn(...arg)
 delete obj.fn
 return res
} 
function f(a,b){
 console.log(a,b)
 console.log(this.name)
}
let obj={
 name:'张三'
}
f.myApply(obj,[1,2])  //arguments[1]

手写bind

this.value = 2
var foo = {
 value: 1
};
var bar = function(name, age, school){
 console.log(name) // 'An'
 console.log(age) // 22
 console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

简单版本

Function.prototype.bind = function(context, ...outerArgs) {
 var fn = this;
 return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
 return fn.apply(context,[...outerArgs, ...innerArgs]);  //返回改变了this的函数,
 //参数合并
 }
}

new失败的原因:

例:

// 声明一个上下文
let thovino = {
 name: 'thovino'
}
​
// 声明一个构造函数
let eat = function (food) {
 this.food = food
 console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
 console.log('func name : eat')
}
​
// bind一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange')  //实际上orange放到了thovino里面
console.log('instance:', instance) // {}

生成的实例是个空对象

new操作符执行时,我们的thovinoEat函数可以看作是这样:

function thovinoEat (...innerArgs) {
 eat.call(thovino, ...outerArgs, ...innerArgs)
}

在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

可new可继承版本

Function.prototype.bind = function (context, ...outerArgs) {
 let that = this;
​
function res (...innerArgs) {
     if (this instanceof res) {
         // new操作符执行时
         // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
         that.call(this, ...outerArgs, ...innerArgs)
     } else {
         // 普通bind
         that.call(context, ...outerArgs, ...innerArgs)
     }
     }
     res.prototype = this.prototype //!!!
     return res
}

9. 手动实现new

new的过程文字描述:

  1. 创建一个空对象 obj;
  2. 将空对象的隐式原型(proto)指向构造函数的prototype。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){
 this.name=name
 this.age=age
}
Person.prototype.sayHi=function(){
 console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)
​
////手动实现new
function create(){
 let obj={}
 //获取构造函数
 let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象    !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果      或者let arg = [].slice.call(arguments,1)
 obj.__proto__=fn.prototype
 let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
 //确保返回的是一个对象(万一fn不是构造函数)
 return typeof res==='object'?res:obj
}
​
let p2=create(Person,'李四',19)
p2.sayHi()

细节:

[].shift.call(arguments)  也可写成:
 let arg=[...arguments]
 let fn=arg.shift()  //使得arguments能调用数组方法,第一个参数为构造函数
 obj.__proto__=fn.prototype
 //改变this指向,为实例添加方法和属性
 let res=fn.apply(obj,arg)

10. 手写promise(常见promise.all, promise.race)

// Promise/A+ 规范规定的三种状态
const STATUS = {
 PENDING: 'pending',
 FULFILLED: 'fulfilled',
 REJECTED: 'rejected'
}
​
class MyPromise {
 // 构造函数接收一个执行回调
 constructor(executor) {
     this._status = STATUS.PENDING // Promise初始状态
     this._value = undefined // then回调的值
     this._resolveQueue = [] // resolve时触发的成功队列
     this._rejectQueue = [] // reject时触发的失败队列
    ​
 // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
 const resolve = value => {
     const run = () => {
         // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
         if (this._status === STATUS.PENDING) {
             this._status = STATUS.FULFILLED // 更改状态
             this._value = value // 储存当前值,用于then回调
            ​
             // 执行resolve回调
             while (this._resolveQueue.length) {
                 const callback = this._resolveQueue.shift()
                 callback(value)
             }
         }
     }
     //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
     setTimeout(run)
 }
​
 // 同 resolve
 const reject = value => {
     const run = () => {
         if (this._status === STATUS.PENDING) {
         this._status = STATUS.REJECTED
         this._value = value
        ​
         while (this._rejectQueue.length) {
             const callback = this._rejectQueue.shift()
             callback(value)
         }
     }
 }
     setTimeout(run)
 }

     // new Promise()时立即执行executor,并传入resolve和reject
     executor(resolve, reject)
 }
​
 // then方法,接收一个成功的回调和一个失败的回调
 function then(onFulfilled, onRejected) {
  // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
  typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
  typeof onRejected !== 'function' ? onRejected = error => error : null

  // then 返回一个新的promise
  return new MyPromise((resolve, reject) => {
    const resolveFn = value => {
      try {
        const x = onFulfilled(value)
        // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
  }
}
​
  const rejectFn = error => {
      try {
        const x = onRejected(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }

    switch (this._status) {
      case STATUS.PENDING:
        this._resolveQueue.push(resolveFn)
        this._rejectQueue.push(rejectFn)
        break;
      case STATUS.FULFILLED:
        resolveFn(this._value)
        break;
      case STATUS.REJECTED:
        rejectFn(this._value)
        break;
    }
 })
 }
 catch (rejectFn) {
  return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
  return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
    MyPromise.resolve(callback()).then(() => error)
  })
}

 // 静态resolve方法
 static resolve(value) {
      return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
  }

 // 静态reject方法
 static reject(error) {
      return new MyPromise((resolve, reject) => reject(error))
    }

 // 静态all方法
 static all(promiseArr) {
      let count = 0
      let result = []
      return new MyPromise((resolve, reject) =>       {
        if (!promiseArr.length) {
          return resolve(result)
        }
        promiseArr.forEach((p, i) => {
          MyPromise.resolve(p).then(value => {
            count++
            result[i] = value
            if (count === promiseArr.length) {
              resolve(result)
            }
          }, error => {
            reject(error)
          })
        })
      })
    }

 // 静态race方法
 static race(promiseArr) {
      return new MyPromise((resolve, reject) => {
        promiseArr.forEach(p => {
          MyPromise.resolve(p).then(value => {
            resolve(value)
          }, error => {
            reject(error)
          })
        })
      })
    }
}

11. 手写原生AJAX

步骤

  1. 创建 XMLHttpRequest 实例
  2. 发出 HTTP 请求
  3. 服务器返回 XML 格式的字符串
  4. JS 解析 XML,并更新局部页面

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON

了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

version 1.0:

myButton.addEventListener('click', function () {
  ajax()
})

function ajax() {
  let xhr = new XMLHttpRequest() //实例化,以调用方法
  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步
  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。
    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。
      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功
        let string = request.responseText
        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
        let object = JSON.parse(string)
      }
    }
  }
  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}

promise实现

function ajax(url) {
  const p = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open('get', url)
    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status <= 300) {
          resolve(JSON.parse(xhr.responseText))
        } else {
          reject('请求出错')
        }
      }
    }
    xhr.send()  //发送hppt请求
  })
  return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
  .catch(reason => console.log(reason))

12. 手写节流防抖函数

函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

节流:连续触发事件但是在 n 秒中只执行一次函数

例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

防抖的实现:

function debounce(fn, delay) {
     if(typeof fn!=='function') {
        throw new TypeError('fn不是函数')
     }
     let timer; // 维护一个 timer
     return function () {
         var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
         var args = arguments;
         if (timer) {
            clearTimeout(timer);
         }
         timer = setTimeout(function () {
            fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
         }, delay);
     };
}

// 调用​
input1.addEventListener('keyup', debounce(() => {
 console.log(input1.value)
}), 600)

节流的实现:

function throttle(fn, delay) {
  let timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {
      return;
    }
    timer = setTimeout(function () {
      fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
      // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
      timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
    }, delay)
  }
}

div1.addEventListener('drag', throttle((e) => {
  console.log(e.offsetX, e.offsetY)
}, 100))

13. 手写Promise加载图片

function getData(url) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url,
      success(data) {
        resolve(data)
      },
      error(err) {
        reject(err)
      }
    })
  })
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
  console.log(data1)
  return getData(url2)
}).then(data2 => {
  console.log(data2)
  return getData(url3)
}).then(data3 =>
  console.log(data3)
).catch(err =>
  console.error(err)
)

14. 函数实现一秒钟输出一个数

for(let i=0;i<=10;i++){   //用var打印的都是11
 setTimeout(()=>{
    console.log(i);
 },1000*i)
}

15. 创建10个标签,点击的时候弹出来对应的序号?

var a
for(let i=0;i<10;i++){
 a=document.createElement('a')
 a.innerHTML=i+'<br>'
 a.addEventListener('click',function(e){
     console.log(this)  //this为当前点击的<a>
     e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
     //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
     alert(i)
 })
 const d=document.querySelector('div')
 d.appendChild(a)  //append向一个已存在的元素追加该元素。
}

参考:

数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3

函数柯里化 https://juejin.im/post/6844903882208837645

节流防抖 https://www.jianshu.com/p/c8b...

查看原文

赞 48 收藏 40 评论 2

九是我呀 发布了文章 · 1月7日

判断对象为空对象

1.空对象判断可以用true还是false???
背景:(背景可不看)在项目中用到了paperjs库,但有点bug在。2021-01-06 20-21-40 的屏幕截图.png
如图,因为content为空对象,所以content.type为undefined。没找到错误的源头,按理我传进去的不是个空对象,所以我只能做以下判断,若content为空对象的话,return true

相关知识
2021-01-07 15-46-35 的屏幕截图.png
空对象不为false或者true,判断应采用:

// 方法1
if (JSON.stringify(data) === '{}') {
    return false // 为空,返回false
}
return true // 不为空

// 方法2
if (Object.keys(object).length === 0) {
    return false // 为空,返回false
}
return true // 不为空

//方法3
for (var i in obj) { // 不为空,则会执行到这一步,返回true
    return true
}
return false // 为空,返回false
查看原文

赞 0 收藏 0 评论 0

九是我呀 发布了文章 · 2020-12-31

前端面经总结合集(他人+自己)2

2020.11.18 小米一面

1 跨域问题

因为浏览器的同源策略导致了跨域。解决方法,JSONP和CORS,整个CORS通信过程,都是浏览器自动完成,不需要用户参与。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。请求分为:简单和非简单请求,简单请求会在头信息中增加一个origin字段,说明来自哪个源,如果origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

2手写链表,和快排
3写的项目中的亮点(好好总结)
4 flex布局
5 eventloop

js是单线程,所有任务分为同步任务和异步任务,同步任务在主线程上执行,形成执行栈,异步任务运行完,会在任务队列放置一个事件,当执行栈中的同步任务执行完,系统能够会读取任务队列,那些事件对应的异步任务会结束等待状态,进入执行栈中。主线程从任务队列中读取事件,这个过程不断循环,称为eventLoop

6继承
7es6新属性
8闭包
9 bind call apply区别,可以手写其中一个吗
10基本类型
11this指向

作为普通函数被调用,非严格模式下指向全局对象;作为对象属性被调用,指向对象;作为构造函数被调用,只想将要被new出的对象;call和apply的应用,指向参数,传入想要this指向的上下文即可

12 节流和防抖
13react生命周期

总结:项目亮点,自己亮点,基础。都是面经上的,多刷刷。大厂用react的比较多

11.23 跟谁学一面

1、 promise用法,包括all,then
2、 setTimeIterval和setTimeOut

哪个是瞬时,怎么用其中一个实现另外一个

3、 给你九个span,手写九宫格
4、 手写将一个数组乱序排列
5、 手写一个冒泡排序
6、 手写一个关于promise的方法?
7、 用过的框架,有写过小程序吗
8、 前后端交互用到cookie,说下这仨的区别
9、 跨域问题简单请求和复杂请求区别
10、为啥想做前端,是自学的前端吗

(小米面的时候也问到了)

查看原文

赞 1 收藏 1 评论 0

九是我呀 发布了文章 · 2020-12-25

常见git命令

前言

实习六个月了,总结了一些常用的github命令:
我们公司用的方法是先把公司库Fork下来,在自己的本地库修改之后push到自己的远程库上,然后提交PR,别人通过之后就合并了。而且一般不用merge,而用rebase使git树看起来是一条直线(我虽浅薄又很通透的认识),开发代码用的是VSCode,这上面也有管理git的工具(这个在公司电脑上很好用,but not mine)

参考

这些博客真的帮助了我很多很多,非常感谢博主们。

流程:

  • 申请账号,添加公钥。我们公司用的是ssh,这个申请权限使用 SSH 连接到 GitHub。时间长了,这里就不都赘述了(其实我忘了)。
  • 详细过程:fork -> clone -> 创建分支 ->修改代码 -> 提交 ->提交PR

可以参见github----向开源框架提交pr的过程

我常用的几个命令

  • git add .
  • git commit
  • git status
  • git fetch upstream
  • git rebase upstream master
  • git push origin master -f
  • git rebase -i HEAD~n
  • git reset
  • git checkout -b branchName
  • git branch -d branchName

不常用的几个命令

VScode自带的或者可以下载的工具
VScode管理工具

以下操作是没有经过fork的,且在master分支下直接操作的流程。如果想切换分支的话,可以执行:git checkout -b checkName
PS:最开始我都在在master分支下执行的,后来需要PR的多了,就慢慢使用了分支这种方式。实在是太方便了。而且VScode自带工具也很方便操作Screenshot from 2020-09-25 10-34-20.png

进入正题

git add

把更改的文件从工作区workspace添加到暂存区stash里面,commit前的必需工作。git diff命令可以查看两者的区别(我不常用)
git add . :把所有更改的文件添加到stage里面
git add fileName
git add

git commit

把缓存区里的文件添加到版本库中,git diff head可以查看两者区别,最好是git commit -m "commit信息",如果直接git commit可能会出错,另外commit message规范

git commit
接着我执行了git add. 把剩下两个文件一起提交。
执行git log 查看提交记录
git log

连接到远程库

git remote -v可以看到连接的是哪个远程库

git push

git push origin master //把自己的提交commit提交到自己的远程库的master分支下。master也可以换成其他分支名
接下来就可以提交PR了

git rebase

常用在
1.多次提交代码,但最后只保留一个commit的。参考rabase合并多个commit
2.提交pr前整理git树。每次提交前都需要保证rebase upstream/master之后(把远程库的跟自己的合并)没有冲突,但其实这个合并不同于merge,rebase类似于把自己的分支直接接到远程master之后,这样整个提交树保证是个直线。
我在1.txt中做了修改:可以通过下图的方式commit修改1.txt
commit之后
如果想合并几个commit,如第二次提交和第一次修改合并,可以执行
git rebase -i HEAD~2
squash的话是保留那条commit信息,即-m后面的那些
合并成功,very nice

查看原文

赞 0 收藏 0 评论 0

九是我呀 发布了文章 · 2020-12-23

前端面经总结合集(他人+自己)1

参考面经(字节): https://segmentfault.com/a/11...

推荐几个学习的网站:
现代JS教程::https://zh.javascript.info/
JS词典:https://developer.mozilla.org...
阮一峰老师的ES6入门:https://es6.ruanyifeng.com/#d...

问题区

1.JS

  1. ES6基本类型 ?
  2. Set,Map区别? Set无序,类似于数组。可以用于数组快速去重。Map类似于json数据,键值对,但是键可以是对象。
  3. 判断数组的几种方法?为什么要用Object.prototype.toString(),数组的.toString()不行吗,输出什么
  4. 讲一下事件流,怎么阻止冒泡?如何移除事件绑定?
  5. ES6 ,ES7新特性?

2.HTMl和CSS

  1. flex常见属性,如何实现类似快手这种视频列表的瀑布流布局(一行两个) flex:1的含义
  2. css动画的实现方式?具体一些
  3. position每种属性的作用

3.编程题

  1. 什么是面向对象?面向对象特性?与面向过程的区别?
    把大象放进冰箱 分别用面向对象,面向过程,函数式编程实现。
  2. 求二叉树每层的最大节点,放入数组输出。(需要自己构造一棵二叉树去验证) 问优化:怎么只用一层循环实现层序遍历
  3. 实现复杂版本的bind,可new可继承
  4. 实现一个函数,将输入的数组转为链表,并实现一个方法向链表指定位置插入值
  5. 两个无序数组合并成一个有序数组,问时间复杂度。 延伸:快排和冒泡排序的时间复杂度,使用场景
  6. 实现toFix函数
  7. 说一下instanceof的原理,实现一下
  8. 知道map方法吗?实现一下。 reduce和基本版都写了
  9. 实现ajax,至少实现get和post方法。这里深挖了get和post具体的传参方式以及ajax中具体是如何实现的
  10. 实现一个模态框组件,用vue和react均可(要能传递确定取消事件函数,有遮罩,居中)
  11. 看代码输出(考察this和const,let区别)

4. 项目

  1. 实习项目,主要关心做的项目的作用和难点
  2. 自己写的最好的组件
  3. react和vue的区别,各自的生命周期
  4. vue 路由,hash和history的区别,你们项目中是怎么进行状态管理的,怎么配置路由的?
  5. 常见的HTTP请求以及每个请求的作用?GET和POST的区别
  6. 前端安全措施,常见的安全问题,XSS?CSRF
  7. https ca证书的作用,SSL层的作用
  8. 路由切换页面的原理?
  9. 如何尽可能发现代码中的bug,有没有一些工具去辅助

5. 计算机相关知识

  1. 你知道的数据结构有哪些?数组是数据结构吗?不是,那为什么?什么是数据结构?你刚刚说到了堆,堆是一种新的数据结构吗?不是,是完全二叉树。什么是完全二叉树?
  2. 树和图的区别? 图的应用场景?
  3. 网络模型以及每层的协议?

6. 其他

  1. 别人的评价
  2. 自身的优点和缺点
  3. 压力最大的时候?怎么排解压力
  4. 你对于以后前端学习的规划?

答案区

1.1 ES6基本类型

String,Bool,Undefined,Object,Symbol,Number,Null

1.2 Set对象详解

参见https://cloud.tencent.com/dev...

  • Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
  • Set特殊值:Undefined和Infinity只能存一个;NaN只能存一个,虽然不相等;
  • Set可与Array互换 :
let arr = new Array([...Set对象])  
let set=new Set(arr)
  • Set作用:并交差集,数组去重。
let a=new Set([1,2,3]);
let b=new Set([2,3,4]); //并集
let union=new Set([...a,...b]);
console.log(union); //交集
let intersect=new Set([...a].filter(x=>b.has(x)));
console.log(intersect); //差集
let diff=new Set([...a].filter(x=>!b.has(x)));
// 数组快速去重
console.log([...new Set(arr)])
1.3 判断数组的几种方法

参见https://juejin.cn/post/684490...

下面几种方法前两个不准确

  1. arr instanceof Array 缺点:不同执行环境下,判断不正确问题
  2. arr.constructor ===Arrray 缺点:constructor可被重写;不同执行环境下,constructor判断不正确问题
  3. Array.isArray(arr)
  4. Object.prototype.toString
    如:Object.prototype.toString.call(arg)==='[object Array]'

instanceof 判断实例是否属于某种类型,逻辑上是按照该实例的__proto__一层层向上找,子类或者后代同样适用。
它假定只有一个全局环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

toString 可以用toString封装一个函数,判断变量的类型

function getType(obj) {
    return Object.prototype.toString.call(obj).slice(8,-1);
}

var a = [1,2,3];
console.log(getType(a)); //Array 

var b = function(){};
console.log(getType(b)); //Function
**typeof(Arr) ** // Object    不可用于判断Array,一般用来判断基本类型

typeof基本用法参见下图
image

1.4 事件流,阻止冒泡,接触事件绑定

https://segmentfault.com/a/11...
1.事件流描述的是从页面中接收事件的顺序,也可理解为事件在页面中传播的顺序。
2.事件就是用户或浏览器自身执行的某种动作。诸如click(点击)、load(加载)、mouseover(鼠标悬停)。
3.事件处理程序响应某个事件的函数就叫事件处理程序(或事件侦听器)。

addEventListener()removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。当这个布尔值为true时,表示在捕获阶段调用事件处理程序;若果是false,表示在冒泡阶段调用事件处理程序。

调用顺序:
捕获阶段,按照由外之内调用事件处理函数;
冒泡阶段,按照由内至内调用事件处理函数;
捕获阶段又在冒泡阶段调用事件处理程序时:事件按DOM事件流的顺序执行事件处理程序,且当事件处于目标阶段时,事件调用顺序决定于绑定事件的书写顺序(ps:目标阶段是指点中的最内节点)
image

阻止冒泡
参见红宝书(第4版P525)

  1. 给子级加上 event.stopPropagation( ),阻止往上冒泡
  2. 在事件处理函数中返回 false,组织了事件本身(默认事件)没搞懂
  3. event.target==event.currentTarget,让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡;没搞懂
1.5 ES5,ES6新特性

属于基础知识,不赘述

2.1 flex
  1. 常见属性:参见阮一峰老师的 http://www.ruanyifeng.com/blo...
  2. 如何实现类似快手这种视频列表的瀑布流布局(一行两个)
  3. flex:1的含义:flex属性是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto。后两个属性可选。这里相当于flex:1 1 auto。

flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。它可以设为跟widthheight属性一样的值(比如350px),则项目将占据固定空间。

2.2 css动画的实现方式?具体一些

https://zh.javascript.info/js...

2.3 position每种属性的作用

positon和display是前端最重要的两个基本属性,一定得好好写
参见:阮一峰老师的https://www.ruanyifeng.com/bl...

3.11 (this、const/let/var)

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:

  • 如果是该函数是一个构造函数,this指针指向一个新的对象
  • 在严格模式下的函数调用下,this指向undefined
  • 如果是该函数是一个对象的方法,则它的this指针指向这个对象

箭头函数出现之后

  • 箭头函数表达式的语法比[函数表达式]更简洁,并且没有自己的this,arguments,super或者new.target
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
  • 由于 箭头函数没有自己的this指针,通过 call()apply() 方法调用一个函数时,只能传递参数(不能绑定this---译者注),他们的第一个参数会被忽略
// Q1 如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window
var a = 1;
function print () {
 console.log(this.a)
}            
print()  //a=1

// Q2 如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。
const obj = {
 a: 2,
 print: function () { console.log(this.a) }
}
obj.print();  //2
​
// Q3 
const obj = {
 a: 3,
 print: function () { console.log(this.a) }
}
obj.print()  //3  与下面的foo()区别在于是全局环境还是obj环境
const foo = obj.print; 
foo()     //undefined      全局情况下调用的,没有全局变量a

// Q3.1   如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
const obj = {
    a: 3,
    printOut:{
        a:4,
        printIn: function(){ console.log(this.a)},
    },
}
obj.printOut.printIn()    //4 
const foo = obj.printOut.printIn;  
foo()     //undefined      全局情况下调用的,没有全局变量a
​
// Q4
const obj = {
 a: 4,
 print: () => { console.log(this.a) }
}
obj.print();                 //undefined     有箭头函数,对象不构成单独的作用域,导致`print`箭头函数定义时的作用域就是全局作用域。
​
// Q5
var a = 5
const obj = {
 a: 6,
 print: () => { console.log(this.a) }
}
obj.print.call({a: 7}); //5         理由同上
​
// Q6 
function Person () {
 this.a = 8
 this.print = function () {console.log(this.a)}
 return {a: 9}
}
​
const p = new Person()  
console.log(p.a)  //9   此时p为构造函数返回的{a:9}

console.log(p.print())  // undefined p是没有print函数的
​
// Q7 
'use strict';
var a = 1;
​
function print () {
 console.log(this.a)
}
print()    //报错 严格模式下,this指向undefined

//考察let,const
// 1.
a = 100;
let a;
a = 10;
function test(num) {
    console.log(a);
    a = num;
}
console.log(a);
test(5);
console.log(a);

// 2.
const a;
a = 10;      //报错
查看原文

赞 0 收藏 0 评论 0

九是我呀 关注了用户 · 2020-12-22

sprina @sprina1997

力求用最简单的思想去理解清楚复杂概念

关注 16

认证与成就

  • 获得 1 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-12-02
个人主页被 302 人浏览