review整理

111 阅读9分钟

js

一、作用域和作用域链

参考:深入理解JavaScript作用域和作用域链

1、作用域
  • 程序源代码定义变量的区域
  • 作用域规定了如何查找变量,也就是确定了当前执行代码对变量的访问权限
  • JavaScript采用词法作用域(静态作用域),所以函数在定义的时候就确定了作用域 而词法作用域相对的是动态作用域,如果采用动态作用域,函数的作用域在调用时才确定
var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();//1

作用域是一个独立的区域,让变量不会外泄、暴露出去。也就是说作用域最大的作用就是隔离变量,不同作用域下同名变量不会冲突。

2、作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

自由变量的取值:要到创建这个函数的那个域”。 作用域中取值,这里强调的是“创建”,而不是“调用”

3、作用域与执行上下文

JavaScript深入之执行上下文栈

JavaScript深入之变量对象

JavaScript深入之作用域链

JavaScript深入之执行上下文

作用域和执行上下文之间最大的区别是:  执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

执行上下文最明显的就是this的指向是执行时确定的, 同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值

二、this指向

JavaScript 的 this 原理

this指的是函数运行时所在的环境

  • 普通函数:指向window
  • 使用call,apply,bind:指向传入的值
  • 作为对象方法被调用:setTimeout传入function指向window,传入箭头函数指向上级作用域
  • 在class方法中调用:指向正在创建的实例
  • 箭头函数:指向上级作用域

三、闭包

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。 闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量变量。 闭包 = 函数 + 函数能够访问的自由变量。

  • 深入回答闭包: 在某个内部函数的执行上下文创建时,会将父级函数的**活动对象**加到内部函数的 `[[scope]]` 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

闭包应用: 1、函数作为参数被传递:

function print(fn) { 
    const a = 200;
    fn(); 
} 
const a = 100; 
function fn() { 
    console.log(a); 
} 
print(fn); // 100

2、函数作为返回值被返回:

function create() { 
    const a = 100; 
    return function () { 
        console.log(a); 
    }; 
} 
const fn = create(); 
const a = 200; 
fn(); // 100

闭包查找自由变量是在函数定义的地方,向上级作用域查找,不是函数执行的地方

手写代码/算法

1、深拷贝

function deepClone(obj = {}) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    }
    let result;
    if (obj instanceof Array) {
        result = [];
    } else {
        result = {};
    }
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            result[key] = deepClone(obj[key])
        }
    }
    return result;
}
// 测试
var test_obj = {
    a: 1,
    b: [
        c: 2,
        d: {e: 3}
    ]
}
console.log(deepClone(test_obj))

2、数组平铺

function flat(arr) {
    const isDeep = arr.some(item => item instanceof Array);
    if(!isDeep) return arr;
    const res = Array.prototype.concat.apply([],arr);
    return flat(res);
}
//测试:
console.log(flat([1,2,[3,4,[5],[6]],[7]]))

3、数组全排列

function doCombination(arr) {
    var count = arr.length - 1; //数组长度(从0开始)
    var tmp = [];
    var totalArr = [];// 总数组
    return doCombinationCallback(arr, 0);//从第一个开始
    //js 没有静态数据,为了避免和外部数据混淆,需要使用闭包的形式
    function doCombinationCallback(arr, curr_index) {
        for(val of arr[curr_index]) {
            tmp[curr_index] = val;//以curr_index为索引,加入数组
            //当前循环下标小于数组总长度,则需要继续调用方法
            if(curr_index < count) {
                doCombinationCallback(arr, curr_index + 1);//继续调用
            }else{
                totalArr.push(tmp);//(直接给push进去,push进去的不是值,而是值的地址)
            }
            //js 对象都是 地址引用(引用关系),每次都需要重新初始化,否则 totalArr的数据都会是最后一次的 tmp 数据;
            var oldTmp = tmp;
            tmp = [];
            for(index of oldTmp) {
                tmp.push(index);
            }
        }
        return totalArr;
    }
}

//测试数组
var arr = [    [1,2,3,4,5],
    ['a','b','c','d'],
    ['成功', '失败']
];
//调用方法
console.log(doCombination(arr));

4、回文数

function reverse (num) {
    if (num < 0) return false;
    var str = num + ''; // 如果判断字符串就不需要转换了
    return str === str.split('').reverse().join('')
}
console.log(reverse(11211))

5、防抖/节流

防抖:高频率触发的事件,在规定时间内,只响应最后一次,在规定时间内再次触发,时间会重新计算。例如:王者荣耀的回程,被打断后时间会重新计算。

function debounce(fn, delay = 500) {
    let timer = null;
    return function () {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            fn.apply(this,arguments);
            timer = null;
        },delay)
    }
}

节流:高频率触发的事件,在规定时间内,只响应第一次

function throttle (fn, delay = 200) {
    let timer = null;
    return function () {
        if (timer) return;
        timer = setTimeout(() => {
            fn.apply(this,arguments);
            timer = null;
        },delay)
    }
}

防抖和节流的使用场景:

  • 防抖:search搜索时,用户不断输入值时,用防抖来节约请求资源。
  • 节流:1、鼠标不断点击触发,单位时间只触发一次;2、监听滚动事件,比如是否滑动底部自动加载更多

6、数组去重

// 一、利用ES6 Set去重(ES6中最常用),这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

function unique (arr) {
    return Array.from(new Set(arr))
}

// 二、利用for嵌套for,然后splice去重(ES5中最常用), NaN和{}没有去重,null直接消失了
function unique (arr) {
    for (let i = 0; i < arr.length; i++) {
        for(let j = i + 1; j < arr.length; j++) {
            if(arr[i] == arr[j]) {
                arr.splice(j,1);
                j--;
            }
        }
    }
    return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))

// // 三、利用indexOf去重 ,新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
function unique (arr) {
    if (!Array.isArray(arr)) {
        return
    }
    let array = [];
    for (let i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) === -1) {
            array.push(arr[i])
        }
    }
    return array;
}

// 四、利用sort()
function unique (arr) {
    if (!Array.isArray(arr)) {
        return;
    }
    arr = arr.sort();
    let array = [arr[0]];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            array.push(arr[i])
        }
    }
    return array;
}

// 五、利用includes,检测数组是否有某个值
function unique (arr) {
    if (!Array.isArray(arr)) {
        return;
    }
    let array = [];
    for(let i = 0; i < arr.length; i++) {
        if (!array.includes(arr[i])) {
            array.push(arr[i])
        }
    }
    return array;
}

// 六、利用hasOwnProperty
function unique (arr) {
    var obj = {}
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

// 七、利用filter
function unique (arr) {
    return arr.filter(function(item,index,arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
        return arr.indexOf(item,0) === index
    })
}

7、手写call

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

Function.prototype.myCall = (ctx, ...args) => {
    if (typeof this != 'function') {
        throw new TypeError('type error');
    }
    ctx = ctx || window;
    const fn = Symbol('fn');
    ctx[fn] = this;
    const res = ctx[fn](...args);
    delete ctx[fn];
    return res;
}
    

通过 call 方法我们做到了以下两点:

  • call 改变了 this 的指向,指向到 obj 。
  • fn 函数执行了。

8、手写apply

Function.prototype.myApply = (ctx, arr) => {
    if (typeof this != 'function') {
        throw new TypeError('type error');
    }
    ctx = ctx || window;
    const fn = Symbol('fn');
    const res = arr ? ctx[fn](arr) : ctx[fn]();
    delete ctx[fn]
    return res;
}

9、手写bind

Function.prototype.myBind = (ctx, ...args) => {
    if (typeof this != 'function') {
        throw new TypeError('error');
    }
    let self = this;
    return function F() {
        if (this instanceof F) {
            return new self(...args,...arguments);
        }
        return self.apply(ctx,[...args,...arguments]);
   }
}

10、实现new

  1. 首先创一个新的空对象。
  2. 根据原型链,设置空对象的 __proto__ 为构造函数的 prototype 。
  3. 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
function myNew(context) {
    const obj = new Object();
    obj.__proto__ = context.prototype;
    const res = context.apply(obj, [...arguments].slice(1));
    return typeof res === 'object' ? res : obj;
}

11、手写深度比较,模拟lodash.isEqual

image.png

// 判断是否是对象或数组
function isObject(obj) {
    return typeof obj === 'object' && obj !== null
}
function isEqual(obj1,obj2) {
    if (!isObject(obj1) || !isObject(obj2)) {
        return obj1 === obj2
    }
    if (obj1 === obj2) {
        return true
    }
    const obj1Keys = Object.keys(obj1)
    const obj2Keys = Object.keys(obj2)
    if (obj1Keys.length !== obj2Keys.length) {
        return false
    }
    for (let key in obj1) {
        const res = isEqual(obj1[key],obj2[key])
        if (!res) {
            return false
        }
    }
    return true
}
const obj1 = {
    a: 100,
    b: {
        c: 200,
        d: 300
    }
}
const obj2 = {
    a: 100,
    b: {
        c: 200,
        d: 300
    }
}
console.log(isEqual(obj1,obj2))

12、split() 和join() 的区别

'1-2-3'.split('-)  // [1,2,3]
[1,2,3].join('-') // '1-2-3'

13、数组pop,push,shift,unshift分别做什么

功能、返回值、是否改变原数组

  • pop:删除数组最后一项,返回删除的项,改变原数组
  • push:在数组最后面追加一项,返回数组长度,改变原数组
  • unshiift:在数组最前面追加一项,返回数组长度,改变原数组
  • shift:删除数组第一项,返回删除的项,改变原数组

纯函数

  1. 不改变原数组(没有副作用)
  2. 返回一个数组

例如:

纯函数:concat、map、filter、slice

非纯函数:pop、push、unshift、shift、some、every、forEach、reduce

14、数组slice和splice的区别

slice切片
let arr = [10,20,30,40]
console.log(arr.slice()) //[10,20,30,40]
console.log(arr.slice(1,3)) // [20,30]

splice剪接
console.log(arr.splice(1,1) // [20]
console.log(arr.splice(1,2,'a') // [10,'a',40]

15、[10,20,30].map(parseInt)返回结果是什么?

[10, NaN, NaN]

[10,20,30].map((num,index)=>{
    return parseInt(num,index)
})

16、ajax请求get和post的区别?

  • get:一般用于查询操作,post:一般用于用户提交操作
  • get请求参数拼接到url上,post放到请求体内(数据提交可更大)
  • 安全性:post易于防止CSRF

17、函数call和apply的区别

    fn.call(this,p1,p2,p3)
    fn.apply(this,arguments)

18、事件代理(委托)是什么?

    const p1 = document.getElementById('p1')
    const body = document.body
    bindEvent(p1,'click',e=>{
        e.stopPropagation()
        alert('激活')
    })
    bindEvent(body,'click',e=>{
        alert('取消')
    })

19、闭包是什么,有什么特性?有什么负面影响?

  • 作用域
  • 自由变量:在函数定义的地方向上级作用域查找,不是在执行的地方

20、同步和异步的区别

  • 基于js是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行

21、异步应用场景

  • 网络请求,如ajax图片加载
  • 定时任务,如setTimeout

22、event loop(事件循环/事件轮询)的机制

  • 同步代码,一行一行放在Call Stack执行
  • 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  • 时机到了,就移动到Callback Queue
  • 如Call Stack为空(即同步代码执行完)Event Loop开始工作
  • 轮询查找Callback Queue,如有则移动到Call Stack执行
  • 然后继续轮询查找(永动机一样)

image.png

23、DOM事件和event loop

  • JS是单线程的
  • 异步(setTimeout,ajax等)使用回调,基于event loop
  • DOM事件也使用回调,基于event loop,但不是异步

24、promise

  • 三种状态

pending(初始化状态),resolved(成功状态),rejected(失败状态) pending ——〉resolved 或 pending ——〉rejected
变化不可逆

  • 状态表现

pending状态,不会触发then和catch
resolved状态,会触发后续的then回调函数
rejected状态,会触发后续的catch回调函数
resolve只会触发then的回调,不会触发catch
reject只会触发catch的回调,不会触发then

  • then和catch改变状态

then正常返回resolved,里面有报错则返回rejected
catch正常返回resolved,里面有报错则返回rejected

const p1 = Promise.resolve().then(() => {
    console.log(100)
})
console.log('p1',p1) // resolved
const p2 = Promise.resolve().then(() => {
    throw new Error('then error')
})
console.log('p2',p2) // rejected
const p3 = Promise.reject("my error").catch(err => {
    console.error(err)
})
console.log('p3',p3) // resolved,触发then
p3.then(() => {
    console.log(200)
})
const p4 = Promise.reject('my error').catch(err => {
    throw new Error('catch err')
})
console.log('p4',p4) // rejected,触发catch
p4.then(() => {
    console.log(200)
}).catch(() => {
    console.log('some err)
})
  • 面试题
第一题
Promise.resolve().then(() => {
    console.log(1) // 1 [resolved]
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3) // 3 [resolved]
})

第二题
Promise.resolve().then(() => {
    console.log(1) // 1
    throw new Error('error1') // [rejected]
}).catch(() => {
    console.log(2) // 2 [resolved]
}).then(() => {
    console.log(3) // 3
})

第三题
Promise.resolve().then(() => {
    console.log(1) // 1
    throw new Error('error1') // [rejected]
}).catch(() => {
    console.log(2) // 2 [resolved]
}).catch(() => {
    console.log(3)
})