js
一、作用域和作用域链
1、作用域
- 程序源代码定义变量的区域
- 作用域规定了如何查找变量,也就是确定了当前执行代码对变量的访问权限
- JavaScript采用词法作用域(静态作用域),所以函数在定义的时候就确定了作用域 而词法作用域相对的是动态作用域,如果采用动态作用域,函数的作用域在调用时才确定
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();//1
作用域是一个独立的区域,让变量不会外泄、暴露出去。也就是说作用域最大的作用就是隔离变量,不同作用域下同名变量不会冲突。
2、作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
自由变量的取值:要到创建这个函数的那个域”。 作用域中取值,这里强调的是“创建”,而不是“调用”
3、作用域与执行上下文
作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
执行上下文最明显的就是this的指向是执行时确定的, 同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
二、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
- 首先创一个新的空对象。
- 根据原型链,设置空对象的
__proto__为构造函数的prototype。 - 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
- 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
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
// 判断是否是对象或数组
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:删除数组第一项,返回删除的项,改变原数组
纯函数
- 不改变原数组(没有副作用)
- 返回一个数组
例如:
纯函数: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执行
- 然后继续轮询查找(永动机一样)
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)
})