前端面试遇到的问题(一)

今天面试,不知怎么说,面试官询问的很多,完全感觉自己的知识储备略微有点不够,下面看看有些啥问题。

问题一:是关于Object类型的数据,如果只改变属性值要如何去监听?
在ES5中,便有defineProperty(obj, prop, descriptor)方法,但此方法是用于直接在一个对象上定义一个新的属性,或者修改一个已存在的属性,并返回这个对象。
obj: 目标对象;
prop: 目标属性;
descriptor: 对定义或修改的属性描述符。

同时还有defineProperties(obj, prop)方法,来修改、设置、监听多个属性
obj: 目标对象;
prop: 目标属性组合而成的新对象;

let obj = {bb:"yang"};
Object.defineProperty(obj,'data',{ //对单个属性进行监听设置等操作
    enumerable: true, //true时,该属性才会出现在对象的枚举属性中
    value: val,//默认值为undefined,可以是任何有效的JavaScript值(数值,对象,函数等)
    writable: true,//true时,value才能被赋值运算符改变
    上面三个属性,是用于设置 ’data‘ 的一些配制,当下面有get或者set时,则不能使用writable与value,否则会报错
    get:function(){
        return data;
    },
    set:function(newValue){
        data = newValue;
        console.log('set :',newValue);
        //需要触发的渲染函数写在这...
    }
});
Object.defineProperties(obj,{
    bb : {
        configurable: false, //true时,该属性才能被删除
        get: function(){
            return bb;
        },
        set: function(value){
            bb = value;
            console.log('b',value);
        }
    },
    data: {
        enumerable: true,
        configurable: false,
        get: function(){
            return data;
        },
        set: function(value){
            data = value;
            console.log('data',value);
        }
    }
});
obj.data = 5;
obj.bb = "ll"

这里会存在一个明显的问题,那就是如果直接 console.log(obj),打印结果:{},但是如果用obj.data又能获取到值:5。

上面两个操作在ES6也有对应的Proxy(target, handler)方法,该方法还可以劫持数组,但兼容性不好

target:目标对象(可以是任何类型的对象,包括原生数组,函数,甚至可以是另一个代理)
handler: 一个对象,其属性是当执行一个操作时定义代理的行为函数

let handler = {
    get: function(target,name){//如果没有属性,则返回默认值37
        return name in target ? target[name] : 37;
    },
    set: function(obj, prop, value){
        if(prop == 'age'){
            if(!Number.isInteger(value)){
            //Number.isInteger,是用来校验数据是不是整数,’10‘,也是false
                throw new TypeError('age属性设置的值非整数!');
            }
        }
    }
}
let p = new Proxy({},handler);
p.age = '123'; //抛出错误
p.age = 123; //正常

查询相关质料时,发现一个很骚的操作:如何让 a==1 && a==2 && a==3 为 true

方法一:
let b = 1;
Object.defineProperty(window,'a',{
    get: function(){
        return b++;
    }
})
方法二:
let a = {
    b: 1,
    toString(){
        return this.b++;
    }
}

问题二:深浅拷贝
目的是为了解决引用数据类型复制的问题。
来,直接上我之前写过的一个方法:

    getType(data){
        // console.log(Object.prototype.toString.call(data));
        return Object.prototype.toString.call(data).slice(8,-1);
    },
    dpClone(obj){
        let that = this, getT = this.getType(obj);
        switch(getT){
            case "Object":
                (function(){
                    let o = {};
                    for(let val in obj){
                        o[val] = that.dpClone(obj[val]);
                    }
                })();
                break;
            case "Array":
                (function(){
                    let arr = [];
                    for(let i = 0; i < obj.length; i++){
                        arr[i] = that.dpClone(obj[i]);
                    }
                })();
                break;
            case "Function":
                return new Function('return ' + obj.toString()).call(that);
            case "Date":
                return new Date(obj.valueOf());
            case "RegExp":
                return new RegExp(obj);
            case "Map":
                return (function(){
                    let m = new Map();
                    obj.forEach((v,k) => {
                        m.set(k,that.dpClone(v));
                    });
                    return m;
                })();
            case "Set":
                return (function(){
                    let s = new Set();
                    for(let val of obj.values()){
                        s.add(that.dpClone(val))
                    }
                    return s;
                })();
            default :
                return obj;
        }
    }

问题三:map()函数
它是定义在Array中,返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
map()函数不会对空数组进行检测,也不会改变原数组。

arr.map(function(item,index,a){
//do something
},thisIndex)

item:当前元素的值; 必须
index:当前元素的索引; 可选
a:当前元素属于的数组对象; 可选
thisIndex:对象作为该执行回调时使用,传递给函数,用作“this”的值

扩充知识:Map集合与Set集合

Map集合

它是一组键值对的结构,具有极快的查找速度。
比如说,用两个数组分存水果与对应价格

let fruits = ['苹果','香蕉','菠萝','蜜桃'];
let prices = [23,15,18,20];

如果需要找菠萝的价格,是不是比较麻烦,要先确定菠萝在fruits数组的位置,然后再到prices数组里取。
而用Map来实现,就很好处理。

let fruitPrices = new Map([['苹果',23],['香蕉',15],['菠萝',18],['蜜桃',20]]);

现在取菠萝价格的就很简单了,

fruitPrices.get('菠萝');

而添加、删除数据也很方便,

fruitPrices.set('凤梨',26);
fruitPrices.delete("香蕉");

注意:如果出现重复设置,后面会将前面的替换,因为,这里是键值对,键重复,后者替换前者。

Set集合

Set与Map不同,它是 的集合,不存 值 ,所以 Set 集合里不存在重复的 键,很多时候,数组去重,就是用Set来操作的。

let setA = new Set(['a','b',1,2,3,'3']);

注意:这里的 3 与 ‘3’ 是不一样的,所以不会被去重。
添加、删除数据

setA.add('e');
setA.delete(3);

在数组去重时,可以用map()函数,以及Set集合来处理。

问题四:slice()与splice()之间的区别
相同:都是数组的内置方法,都可以用来截取数组的。

不同:
1.入参不一样,
slice,
一个参数时,便是截取的起始下标,一直到数组最后;
两个参数时,则是截取的起、止下标,

注意:slice截取,含起不含止,很多这种类似需要起始位置的操作,基本都是
同时,下标是可以为负值,当截止为负值,则说位置是从后往前数,记住一点 -1 表示数组最后一位,-2表示数组倒数第二位。举个例子:

let arr = [1,2,3,4,5,6,7,'a','b','c',8,9];
console.log(arr.slice(-2,-1));//输出[8],起始是8,结束是9(不包括)

splice,
一个参数,从下标开始截取到数组最后一个;
两个参数,第一个参数是截取起始位置,第二个参数是截取个数;
三个参数以上,第一个参数是截取起始位置,第二个参数是截取个数,第三个参数即后面的参数,都是替换被截取的位置。

let arr = [1,2,3,4,5,6,7,'a','b','c',8,9];
arr.splice(2,1,'fg','lk')
console.log(arr);//输出[1, 2, 'fg', 'lk', 4, 5, 6, 7, 'a', 'b', 'c', 8, 9]

2.截取的效果也不一样
slice返回的是生成新的数组,不影响原数组,只是截取操作。
splice返回的是不需要的部分,而原数组也变成了我们需要的部分,这是直接在原数组上操作,可以截取、替换,插入操作。

问题五:Promise
目的:解决回调深渊的问题,让代码看起来更加舒服,更容易理解与书写。

这里面存在一些问题,第一,Promise一旦新建就会立即执行,无法中途取消;第二,如果没有设置回调函数,Promise内部抛出错误,不会反应到外部;第三,当处于pending状态时,无法得知目前进展到哪一个阶段。
pending:待定;
fulfilled:解决(resolved);
rejected:拒绝;

刚刚细查了一下Promise的资料,加上自测,发现一些我自己没有想到的问题,下面总结一下。

1、Promise里面是一个方法,这个方法里面有两个固定参数,两个参数都是方法,参数一 (res) 表示成功,参数二 (rej) 表示失败,res(值) 会被后面紧跟着的.then(res1,rej1),里面的res1接收到,rej(值) 会被rej1接收,这里接收的是括号里面的--值。
这个两个方法参数,只要有一个执行,便直接进入后续。
注意,这个--值,可以是普通数据类型,也可以是引用数据类型,还可以是一个方法,等。

2、.then(res,rej)里面的rej,是可以接收到前面未接收过的 rej(值) ,如果当前的rej里面有返回值,会被下一个.then(res1,rej1)里面的res1所接收

3、.catch(rej=>{}),这里是捕获错误,但前提是,前面的then里面没有第二个参数rej才行,一旦某个then里面有rej,那么前面的失败是走不到catch里面,catch也是有返回值的,如果后面再接一个then也是可以接收到这个返回值。

4、.finally(),它没有入参,只要写上去就会执行,不管是中间,还是最后,建议是最后,这个方法类似最后的收尾工作,
个人建议,在有.finally()时,catch写在它前面一个就行。

如果看不明白,可以拿我这个自测的代码玩一下,

let promise = new Promise(function(res,rej){
    // res("su");
    rej(1);
  }).then(res1=>{
    console.log('res1',res1);
  },rej1=>{
    console.log('rej1',rej1);
    return 0;
  }).then(res2=>{
    console.log('res2',res2);
    return 'res2'
  },rej2=>{
    console.log('rej2',rej2);
  }).catch(rej3=>{
    console.log('catch',rej3);
    return 2;
  }).finally(()=>{
    console.log("finally");
  })

上面的都是串行,一个接一个的操作,多个并行,Promise自然也有:Promise.all()方法

let fn1 = function(){};
let fn2 = function(){};
let arr = [fn1,fn2];//fn1,fn2都是异步函数
Promise.all(arr).then(([data1,data2])=>{}).catch((rej)=>{})

该方法存在一个问题,如果其中一个方法出问题,大家一起GG。
所以,在有已知异步函数的情况下,还是串行相对要好。

那如果存方法的数组里不确定有多少个异步方法?
于是乎,用reduce()来改进

let arr = [fn1,fn2,…];
arr.reduce((task,promise)=>{
    return task.then(()=> return promise).then(res=>{})
},Promise,resolve())

面对数组的reduce()方法,我又开始进行探索,(喵的,写了快一整天了,居然才写到问题五,这样研究下去,可以研究一个星期)
来来,介绍一下reduce(callback,[initVal])
这个方法类型for循环跟forEach方法的功能类似,就是遍历。

参数一:
callback(prev,cur,index,arr):就是一个回调函数,不过这回调函数的入参有点子多--四个
prev: 在reduce有第二个参数initVal时,prev的第一次就是initVal,记住是第一次,第二次开始就是前面处理的返回值。
如果reduce没有第二个参数,那么他就是数组的第一个元素,而下面的cur变成了第二个
cur:当前被遍历到的元素
index:当前元素的下标
arr:就是调用reduce的数组

参数二:
initVal:作为callback第一次调用时的入参;

下面代码,大家拿回去自测一下

var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index);
    return prev + cur;
})
console.log(arr, sum);

最开始不是说Promise一旦建立就没法中途取消,我就想着,能不能搞点事情,比如说异步处理中定一个超时。
想了一下,真的取消是不可能,那就只能利用Promise的机制,通过reject,resolve让他直接跳到最后的catch。

function stopPromise(fn,times){ //fn可以是reject,也可以是resolve
    setTimeout(()=>{
        fn(定义传给后面的数据)
    },times)
}
//使用
function myPromise(callback){
    return new Promise((res,rej)=>{
        //处理代码
        callback && callback(rej,5000);//上面代码5s内没有处理,便直接跳到下一步
    })
}
myPromise(stopPromise).then().catch()

然后我又百度了一下资料,发现还有另外一种方法,Promise.race()
科普Promise.race():该方法也是传入 异步函数数组,与Promise.all()类似。

不同之处:
all()的返回值是大家一起执行完,将结果组成一个新数组返回,其顺序是根据入参数组里的顺序排列的,
race()谁先执行完,就返回那个结果,不管结果是成功还是失败。
因此,利用这个特点来操作。

let myStop = new Promise((res,rej)=>{
    setTimeout(()=>{
        //res()或rej()
    },5000)
})
let myPromise = new Promise(res,rej)=>{
    //执行代码
})

Promise.race([myStop,myPromise]).then().catch()

这里有一个狠明显的问题,那就是超时时间不能随心所欲。

问题六:变量声明的区别
let声明的变量不能在声明之前使用;
var声明的变量很随意,这也是ES6 新增两个声明的原因;
const声明的变量是不允许修改的,不过对象里修改属性,或者新增属性,不会报错,因为地址没变

问题七:ES6中some与every之间的区别
既然都说了ES6的方法了,那索性就一并拿出来瞅瞅。

数组相关

filter方法,用来过滤数组,生成一个新的数组

let newArr = arr.filter((val,index,arr)=>{
    //val当前元素,index当前元素下标,arr原数组
    //操作过程
    return true; 返回值为true,则当前元素返回
})

reduce方法前面说了,就不复述了
reduceRight方法,与reduce方法一样,但是,它是从数组右边往左遍历
Array.from方法,将类数组转为数组,只要是含有length属性的都可以转,
最骚的是,如果一个对象里面有length属性,也能转,看例子

let str = 'abcd';
let strArr = Array.from(str);//['a','b','c','d']

let obj = {name:"test",age:18,2:'two',5:'fi','3':'three',length:6}
console.log(Array.from(obj));//输出[undefined,undefined,'two','three',undefined,'fi']

从这个例子,大家应该看出点门道来了吧,第一个就不说了,第二个能转,就是一个 length:6 这个属性,就转成了一个长度为6的数组,转换的规矩是看键名,是否有 0-5 之间的 数字与字符串,匹配上,其键值就是新数组对应位置上的值,匹配不上的位置就是undefined。

Array.of方法,将一组值转换数组,类似声明一个数组(new Array())

Array.of('123');//['123']
Array.of({a:23,b:4})//[{a:23,b:4}]
new Array('33');//['33']

copyWithin方法,在数组内部将指定的一段数组,复制到其他位置,会改变原数组
参数一,替换的开始位置,必传
参数二,指定数组的起始下标,默认0,为负值,则从右向左(类似splice方法里为负值),可选
参数三,指定数组的结束下标,默认为数组长度,为负值,表示倒数,可选

let arr = [1, 2, 3, 4, 5];
  // console.log(arr.copyWithin(3));       [1, 2, 3, 1, 2]
  // console.log(arr.copyWithin(0,3));     [4, 5, 3, 4, 5]
  // console.log(arr.copyWithin(0,3,4));   [4, 2, 3, 4, 5]
  
  // console.log(arr.copyWithin(-1));      [1, 2, 3, 4, 1]
  // console.log(arr.copyWithin(-2,-3));   [1, 2, 3, 3, 4]
  //console.log(arr.copyWithin(-5,-3,-1)); [3, 4, 3, 4, 5]

find方法,找出第一条符合条件的数组项

let arr = [2,3,4,5]
arr.find((item,index,arr)=>{
    // item当前元素,index当前元素下标,arr当前数组
    return item > 3;//返回4
})

findIndex方法,找出第一条符合条件的数组想的下标,与find方法一样操作

fill方法,使用指定值填充整个数组,会改变原数组
参数一,填充值
参数二,开始填充的起始下标
参数三,结束填充的截止下标,不包括

let arr = [1, 2, 3, 4, 5];
console.log(arr.fill('a',2,4));//[1, 2, 'a', 'a', 5]

some方法,数组迭代方法,用来判断数组里面有没有符合要求的数据,只要有一个满足,返回true,没有符合的返回false
every方法,数组迭代方法,用来判断数组里面的所有数据是否符合要求,必须要全部符合才会返回true,有一个不符合,返回false

[44,32,54,12].some((item,index,arr)=>{return item > 50})  //true
[44,32,54,12].every((item,index,arr)=>{return item > 50}) //false

keys方法,遍历数组的键名(一般针对Map/Set集合)

let arr = [1,2,3,4];

//keys方法
let arr2 = arr.keys();
console.log(arr2)//打印 Array Iterator {}
for(let key of arr2){
    console.log(key);//0,1,2,3
}

//value方法
let arr3 = arr.value();
console.log(arr3);//Array Iterator {}
for(let val of arr3){
    console.log(val);//1,2,3,4
}

//entries方法
let arr4 = arr.entries();
console.log(arr4);//Array Iterator {}
for(let item of arr4){
    console.log(item);//[0,1],[1,2],[2,3],[3,4]
}

有没有发现上面的那个奇怪的问题,直接打印数组是没有什么数据,但遍历还是有数据的,ES6新增的Object.defineProprety()、Object.definePropreties()中的set操作也会有类似的问题
value方法,与keys()方法相对应,它遍历键值(一般针对Map/Set集合),案例就写上面了
entries方法,结合keys与value,遍历数组的键值与键名(一般针对Map/Set集合),案例就写上面了方便一起对比

for…of与for…in的区别

for…of只能遍历数组,而for…in能遍历对象和数组(下标可以视为键,数据项为值)
如果动态给数组添加一个键值对,for…in会数组的键包括添加的属性名一起遍历,而for…of只会遍历原数组据项

字符串新增方法

includes方法,校验字符串中是否包含指定字符串,返回true、false
参数一:指定字符串;
参数二:查找起始下标位置

let str = 'ewwrfsdfsf';
console.log(str.includes("ww")); //true
console.log(str.includes("ww",2));//false,从下标2开始往后查,是匹配不到的

console.log(str.startsWith("ww",1));//true

console.log(str.endsWith("ww",3));//true

startWith方法,校验字符串是特定位置开始,是否以特定字符开头
参数一:指定字符串
参数二:起始下标
案例写在上面了。

endsWith方法,校验字符串是否以特定字符串结尾,
参数一:指定字符串
参数二:结束下标
案例写上面

repeat方法,重复当前字符串,不会影响原数组
参数:重复次数

let str1 = 'abc';
console.log(str1.repeat(2));//abcabc
console.log(str1);//abc

padStart方法,字符串首位补全,生成新字符串
参数一:字符串的长度
参数二:补充的字符串

let str2 = 'efgj';
console.log(str2.padStart(6,'12'));//12efgj
console.log(str2.padStart(8,'12'));//1212efgj,补充字符串长度不够,则重复替换
console.log(str2.padStart(8));//    efgj,没有传补充字符串,则以空格代替
console.log(str2.padStart(8,'123456'));//1234efgj,补充字符串长度过长,则截取前面部分


console.log(str2.padEnd(6,'12'));//efgj12,
console.log(str2.padEnd(8,'12'));//efgj1212,
console.log(str2.padEnd(8));//efgj    ,
console.log(str2.padEnd(8,'123456'));//efgj1234

padEnd方法,与padStart方法相反,从字符串末尾补全
参数一:字符串长度
参数二:补充字符串
案例写上面

trimStart方法,过渡字符串前面的空格部分,
trimEnd方法,过滤字符串后面的空格部分,
trim方法,过滤字符串前后的空格部分

let str3 = '   hjk  ';
console.log(str3.trim());     //hjk,
console.log(str3.trimStart());//hjk   ,
console.log(str3.trimEnd());  //   hjk,

replace方法,替换字符串中所以指定的字符片段
参数一:被替换的字符片段
参数二:替换的字符片段

let str4 = 'bcdfjk';
console.log(str4.replace('df','hhh'));//bchhhjk
console.log(str4.replace('dj','hhh'));//bcdfjk

问题八:forEach如何跳出循环
首先,return false是没有用的,然后break、continue这些也不管用。
查了一下资料,forEach、map这两个方法都是不能中途终止的,除非抛出异常错误才能终止(遍历完不算),所以,想终止那就手动抛异常,
还有一点要注意,如果用throw new Error(),那就得用try{}catch(e){}来捕捉这个错误,不然代码就 走不下去了。

问题九:vue v-for与v-if
vue 2.X版本里,同一个元素上,v-for的优先级是大于v-if的,在同一个标签一起使用就有点耗费性能,一般情况下,会选择通过computed计算属性,将if判断为true的数据筛选,然后筛选的数据拿去v-for。
网上还有一个操作,避免渲染本该隐藏的列表项,将v-if放到template,将v-for包括起来。
但是,在vue 3.X版本里,v-if总是优先与v-for生效

问题十::key的基础,为什么index没有id好(v-for里最为明显)
首先,咱先说说:key的作用,它相当于给DOM对象加了一个标识,方便在diff算法执行时,能更快的找到对于的节点,高效的更新虚拟dom。
如果DOM发生变化,Vue里面首先要做的是,生产最新的虚拟DOM,然后拿旧的虚拟DOM来与之对比,对比就是以:key绑定的值为准,如果值相同,就直接拿来用,如果不同则重新创建。
这样就可以看出,用index的问题,如果新值是从数据最前面插入的,那么v-for遍历修改后的数据时,是不是每个新的虚拟DOM都要重新创建,而老的虚拟DOM都要删除,而用id作为唯一标识,那么就只需要将新增的虚拟DOM加入就行。

拓展一下:
如果没有:key,那么vue会使用一种最大限度减少动态元素并尽可能的尝试就地修改/复用相同类型元素的算法。
说一下响应式数据更新后,是怎么个操作,
先会触发 渲染Watcher 的回调函数 vm._update(vm._render()) 驱动视图更新,
vm._render() 其实生成的就是 vnode ,而 vm._update 就会带着新的 vnode 去触发 patch
patch的过程:
1、不是相同节点:isSameNode为false,直接销毁旧的 vnode ,渲染新的 vnode。
2、是相同的节点,就会继续往下对比,尽快能做到节点的复用
这里会调用 src/core/vdom/patch.js 里的patchVNode方法
新vnode是文字:直接调用浏览器的 dom api 将节点直接替换文字内容。
新vnode不是文字:那就要开始对比 子节点 children,一直类推。
有新children没有旧children:直接 addVnodes 添加新子节点
无新children有旧children:直接 removeVnodes 删除旧子节点
写到这里我想到一个问题:

  • {{val}}
methods:{ addFor(){ this.vFor.unshift(8); } }

这一部分代码放入vue中,点击按钮会出现什么情况?


vFor1.png

vFor2.png

看到没有,很骚气吧,直接复用了!
而且,哪怕在input里加入了:key="index",也是这个结果,这也是在说明index的不足之处

问题十一:v-model这个语法糖是优化那些方法的,原理是什么?
v-model即可以作用于表单元素,又可作用于自定义组件,最终会生成一个属性和一个事件。
当其作用于表单元素时vue会根据作用的表单元素类型而生成合适的属性和事件。例如,作用于普通文本框的时候,它会生成value属性和input事件,而当其作用于单选框或多选框时,它会生成checked属性和change事件。
当其作用于自定义组件时,默认情况下,它会生成一个value属性和input事件;可以通过组件的model配置来改变生成的属性和事件

问题十二:compute与watch直接的区别,以及他们的使用
1、computed支持缓存,只有当依赖的数据发生变化,才会重新计算。而watch不支持缓存,数据变化,立即出发相应操作。
2、computed不支持异步,异步操作在computed内是无效的,无法监听到数据变化。而watch支持异步。
3、computed属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中的声明过或者父组件传递的props中的数据通过计算得到的值;watch监听的函数接收两个参数,第一个参数是最新值,第二个参数是变化之前的旧值
4、如果一个属性是由其他属性计算而来,这个属性依赖其他属性,是一对一或者一对多,或者说多个数据影响该计算属性,一般就用computed。如果属性变化时,需要执行一些操作,或者该数据会影响多个数据,一般就是watch
5、在computed中,属性都有get和set方法,如果computed的属性值是函数,那么就会走get方法,函数的返回值就是属性的属性值,如果依赖的数据变化,则调用set方法。

一次面试,总结三天,我太难了!
写了近七千字的总结,都快写吐我了。
嘿嘿!其实,还有两个问题,vuex状态管理,以及为何不用浏览器缓存而是用vuex来管理数据,(刚刚准备上传文章时,才想起来)
能看到最后的小伙伴,可以自己思考一二,然后留个言吧!!!

你可能感兴趣的:(前端面试遇到的问题(一))