JS组成:
JS是运行在浏览器的一门编程语言
函数类型:
1. 说说 js 都有哪些数据类型,他们在内存存储上有什么不同
基本数据类型:number、boolean、string、null(null就是特殊的object)、undefined、Symbol(ES6新增,表示唯一标识符)
引用数据类型:object、function、array
(内置引用类型:Math、Date、Number、String 等)
内存存储: 基本数据类型存储在栈内存,
引用数据类型存储在堆内存,栈内存中只存储引用地址
PS:判断数据类型
(1)typeof
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object
console.log(typeof 1);
// number
console.log(typeof null);
// object
(2) instanceof
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断
console.log(1 instanceof Number);
// false
console.log(true instanceof Boolean);
// false
console.log('str' instanceof String);
// false
console.log([] instanceof Array);
// true
console.log(function(){} instanceof Function);
//true
console.log({} instanceof Object);
// true
(3)Object.prototype.toString.call()
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
var toString = Object.prototype.toString;
console.log(toString.call(1));
//[object Number]
console.log(toString.call(true));
//[object Boolean]
console.log(toString.call('mc'));
//[object String]
console.log(toString.call([]));
//[object Array]
console.log(toString.call({}));
//[object Object]
console.log(toString.call(function(){}));
//[object Function]
console.log(toString.call(undefined));
//[object Undefined]
console.log(toString.call(null));
//[object Null]
2. js 是如何进行垃圾回收的?
通过引用计数统计引用次数,若为0则自动回收
3. var let const 的区别
var 函数作用域,具有变量提升作用,可重复声明
let 块级作用域,不可重复声明,可变
const 块级作用域,不可重复声明,不可变
4. 说说 js 的作用域
全局作用域:在浏览器的控制台,script中,以及不在函数中的代码都是在全局作用域,如window变量
函数作用域:函数执行时的上下文,外部无法访问其中的变量,不会污染全局变量
块级作用域:for,while,{}中的代码
模块作用域:ES6或node中一个一个文件,相互隔离,不会污染全局变量
5. 什么是作用域链
首先在创建该变量的当前作用域中取值,当前作用域找不到,继续到上级作用域中查,直到查到全局作用域,这个查找过程形成的链条就做作用域链。
6. 什么是闭包,有什么用
JS闭包:内层函数+引用外层函数的变量(一个大函数里包含了一个变量+一个内部函数)
对闭包内的变量起到一个保护性的作用,外部不可直接使用此变量。
闭包不一定有return,不一定会有内存泄漏。
什么时候用到return?
当外部想用闭包变量时就用return,把局部变量返回到外面来,但是外面可以使用此变量但不能修改。
闭包的应用:实现数据的私有,对闭包内变量起到一个保护性作用和保存作用
7. 经典闭包 for 循环题目
<script>
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// 0,1,2,3,4
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// 5,5,5,5,5
</script>
对下面的函数进行修改使其输出0,1,2,3,4
for (var i = 0; i < 5; i++) {
// 增加外部函数
function outer() {
// 通过闭包保存i
var index = i;
function inner() {
console.log(index);
}
return inner;
}
var func = outer();
setTimeout(func, i * 1000);
}
简化:
for (var i = 0; i < 5; i++) {
setTimeout(
(
(i) => () =>
console.log(i)
)(i),
i * 1000
);
}
简化过程:
8. js 中的原型了解吗?说说原型和原型链。为什么要用到原型,直接用点方法不好吗?
(1)原型:每个函数都有prototype
属性,称之为原型,这个属性是一个对象,也称为原型对象。
起作用是存放一些属性、方法;在JS中实现继承
__proto__
:每个对象都有该属性,指向原型
原型链:对象都有__proto__
属性,此属性指向它的原型对象。原型对象也是对象,也有__proto__
属性,指向原型对象的原型对象。这样一层一层形成的链式结构叫原型链,最顶层找不到则返回null
(2)JS中的原型通过面向对象来实现,prototype上的属性是所有实例共享的,可以节省内存。如果定义一个对象,比如一个数组arr=[1,2,3],调用arr.push(4)方法不是在原型上,而是在每一个对象上,那么每调用一次就要占用一次内存。但是定义到原型上,所有实例的方法只占据一片空间。
prototype 上的属性,各个实例都是共享的,this 中的属性,都是各自占据一片空间
9. call,apply,bind 有什么区别?(能自己实现一个吗?)
它们都定义在Function.prototype上,任何一个函数都可以访问到call,apply,bind方法,其作用是修改函数 this 的指向,第一个参数都是想要指定的 this 的值。
call()将实参在对象后依次传递 a,b,c,d
apply()需将实参封装到一个数组中统一传递 [a,b,c,d]
bind() 也是依次传递参数,返回改过this指向的新函数,调用才会执行,即bind(a,b,c,d)() a,b,c,d
自己实现call:
// 手写实现call
Function.prototype.myCall = function (context, ...args) {
// 先判断调用对象是不是函数
if (typeof this !== "function") {
console.log("type error");
}
// 判断context是否传入,null则设为window
context = context || window;
// 增加context的临时属性fn,用来存储原来的this指向,也就是函数自己,方便后面调用(为了避免fn与context本身属性重复,使用symbol)
const fn = Symbol();
context[fn] = this;
// 调用方法,此时使用的是context的方法,那么fn 属性所引用的函数(即原始函数)在执行时,this 将指向 context 对象
const result = context[fn](...args);
// 删除临时属性,避免 context 对象被 fn 属性污染
delete context[fn];
// 返回函数本身调用结果
return result;
};
自己实现apply:
自己实现bind:
// 手写实现bind
Function.prototype.myBind = function (context, ...args) {
// 先判断调用对象是不是函数
if (typeof this !== "function") {
console.log("type error");
}
// 判断context是否传入,null则设为window
context = context || window;
// 闭包存一下当前函数,此时this指向要调用的函数
const fn = this;
return function (...innerArgs) {
// 由于bind语法与call语法类似,此处利用call帮助实现
return fn.call(context, ...args, ...innerArgs);
// 也可以使用apply实现
return fn.apply(context, args.concat(innerArgs));
};
};
10. new 字段发生了什么,可以自己实现吗?
创建了一个新的空对象,构造函数this指向新对象,执行构造函数,给新对象添加属性值,最后返回新对象。
手写new:
// 改进版:如果构造函数有返回值且返回值是个对象,那么实力直接返回该返回值
function myNew2(constructor, ...args) {
// 创建新的空对象
const obj = {};
// 设置改空对象的__proto__指向构造函数的prototype(为新对象增加属性)
// obj.__proto__ = constructor.prototype;
Object.setPrototypeOf(obj, constructor.prototype);
// apply调用constructor,修改this指向obj
const res = constructor.apply(obj, args);
// 如果构造函数返回对象而且不是null,那么myNew2就返回该对象,否则返回obj
return typeof res === "object" && res !== null ? res : obj;
}
11. for in 和 for of 有什么区别?
for...in
和 for...of
语句都用于迭代某个内容,它们之间的主要区别在于迭代的对象。
for...in
语句用于迭代对象的可枚举字符串属性,而 for...of
语句用于迭代可迭代对象定义的要进行迭代的值。
前者迭代属性名称,后者迭代属性值。
for in 通常不推荐用于迭代数组,因为它不保证迭代顺序,并且可能会遍历到数组的原型链上的属性。
12. 数组都有哪些迭代方法,他们会在原数组上改变还是返回新数组?
sort、splice、forEach、reverse、push/pop、unshift/shift
会在原数组上进行改变,map、filter、concat
等会返回新数组
13. 手写深拷贝和浅拷贝
直接复制对象是复制地址,会影响原对象。
浅拷贝只适合单层数据,单层不影响,多层有影响
深拷贝不影响原数据
手写浅拷贝:
const obj = {
uname: "pink",
age: 18,
family: {
baby: "小pink",
},
// 1. 浅拷贝,拷贝的是地址(适合单层,单层不影响,多层会影响)
// (1)扩展运算符
const shadowCopy = { ...obj };
console.log(shadowCopy);
shadowCopy.age = 20;
console.log(shadowCopy);
console.log(obj); //不变
// (2)Object.assign()
const shadowCopy1 = {};
Object.assign(shadowCopy1, obj);
shadowCopy1.age = 22;
// shadowCopy1.family.baby = "小red";
console.log(shadowCopy1);
console.log(obj); //外面18不变,里面那层变了
手写深拷贝:
// 2. 深拷贝,拷贝的是对象不是地址
// 新对象不会影响旧对象
// 需要用到递归,遇到普通数值对象直接赋值,遇到数组再次调用递归函数,遇到对象也再次利用递归调用函数。先数组后对象
// (1) 递归实现:函数内部再去调自己
// (1) 递归实现:函数内部再去调自己
const obj = {
uname: "pink",
age: 18,
hobby: ["music", "study"],
family: {
baby: "小pink",
},
};
const o = {};
// 拷贝函数
// Array和Object判断不能更换顺序,因为数组也属于对象
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组的问题
if (oldObj[k] instanceof Array) {
newObj[k] = [];
// newObj[k] 接收方 []
deepCopy(newObj[k], oldObj[k]);
} else if (oldObj[k] instanceof Object) {
newObj[k] = {};
// newObj[k] 接收方 {}
deepCopy(newObj[k], oldObj[k]);
} else {
// k 属性名 oldObj[k]属性值
newObj[k] = oldObj[k];
}
}
}
// 函数调用 两个参数:新对象 旧对象
deepCopy(o, obj);
console.log(o);
o.age = 20;
o.hobby[0] = "basketball";
o.family.baby = "老pink";
console.log(obj);
// 数组也属于对象
console.log([1, 2, 3] instanceof Object); //true
14. 了解函数防抖和节流,尝试手写
防抖: 连续触发事件但是在设定的一段时间内只执行最后一次(强调只要打断就重新开始)
例如:设定1000毫秒执行,当你触发事件了。他会1000毫秒后执行,但是在还剩500毫秒的时候你又触发了事件,那就会重新开始1000毫秒之后再执行
应用场景:搜索框搜索输入、文本编辑器实时保存、手机号邮箱输入检测
节流: 连续触发事件但是在设定的一段时间内只执行一次函数(强调不要打断我)、
例如:设定1000毫秒执行,那你在1000毫秒触发在多次,也只在1000毫秒后执行一次
应用场景:高频事件(快速点击、鼠标移动mousemove、下拉加载、scroll事件(页面滚动触发)、resize事件(页面尺寸改变触发))、视频播放记录时间等
手写防抖:
核心思路:利用定时器(setTimeout
)来实现
setTimeout
只能执行一次,而setInterval
会反复执行
(1) 先声明一个定时器变量
(2) 当鼠标每次滑动先判断是否有定时器了,若有先清除之前的定时器
(3) 若没有定时器则开启定时器,记得存到变量里面
(4) 在定时器里调用要执行的函数
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
// 利用防抖实现性能优化
// 需求: 鼠标在盒子上移动,里面的数字就会变化+1
// 浪费性能,优化:鼠标停止500ms以后,里面的数字才会变化+1
const box = document.querySelector(".box");
let i = 1;
function mouserMove() {
box.innerHTML = i++;
// 如果里面存在大量小号性能的代码,比如dom操作、数据处理,可能造成卡顿
}
// 添加事件:鼠标一移动就会触发mouseMove事件
// box.addEventListener("mousemove", mouserMove);
// 1. lodash提供的防抖处理 _.debounce(func,waitTime)
// 500ms之后采取+1
box.addEventListener("mousemove", _.debounce(mouserMove, 500));
// 2. 手写防抖函数来处理
// 核心思路:利用定时器(setTimeout)来实现
// setTimeout只能执行一次,而setInterval会反复执行
// (1)先声明一个定时器变量
// (2)当鼠标每次滑动先判断是否有定时器了,若有先清除之前的定时器
// (3)若没有定时器则开启定时器,记得存到变量里面
// (4)在定时器里调用要执行的函数
function debounce(fn, t) {
let timer;
// return返回一个匿名函数
return function () {
if (timer) clearTimeout(timer);
// function () {} 匿名函数
timer = setTimeout(function () {
fn(); //加小括号调用fn函数
}, t);
};
}
// 我们想每次鼠标移动都要执行一下匿名函数里的所有代码
box.addEventListener("mousemove", debounce(mouserMove, 500));
</script>
手写节流:
核心思路:利用定时器(setTimeout
)来实现
setTimeout
只能执行一次,而setInterval
会反复执行
(1) 声明一个定时器变量
(2) 当鼠标每次滑动都先判断是否有定时器了,如果有定时器咋不开启新定时器
(3) 如果没有定时器则开启定时器,记得存到变量里面
定时器里面的操作:定时器里面调用执行的函数;定时器里面要把定时器清空
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
// 节流:单位时间内频繁触发事件只执行一次
// 要求:鼠标在盒子上移动,不管移动多少次,每隔500ms才+1
const box = document.querySelector(".box");
let i = 1;
function mouserMove() {
box.innerHTML = i++;
// 如果里面存在大量消耗性能的代码,比如dom操作、数据处理,可能造成卡顿
}
// box.addEventListener("mousemove", mouserMove);
// 1. 利用lodash库实现节流
// _.throttle(func,waitTime) 在waitTime最多执行func一次的函数
// box.addEventListener("mousemove", _.throttle(mouserMove, 3000));
// 2. 手写节流函数来处理
// 核心思路:利用定时器(setTimeout)来实现
// setTimeout只能执行一次,而setInterval会反复执行
// (1) 声明一个定时器变量
// (2) 当鼠标每次滑动都先判断是否有定时器了,如果有定时器咋不开启新定时器
// (3) 如果没有定时器则开启定时器,记得存到变量里面
// 定时器里面的操作:定时器里面调用执行的函数;定时器里面要把定时器清空
function throttle(fn, t) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(function () {
fn();
// 时间到了清空定时器
// 在setTimeout中是无法删除定时器的,因为定时器在运作,所以使用timer = null 而不是 clearTimeout(timer)
timer = null;
}, t);
}
};
}
box.addEventListener("mousemove", throttle(mouserMove, 500));
</script>
15. 了解下函数柯里化
柯里化是编程语言中的一个通用的概念(不只是Js,其他很多语言也有柯里化),是指把接收多个参数的函数变换成接收单一参数的函数,嵌套返回直到所有参数都被使用并返回最终结果。
更简单地说,柯里化是一个函数变换的过程,是将函数从调用方式:f(a,b,c)变换成调用方式:f(a)(b)©的过程。柯里化不会调用函数,它只是对函数进行转换。
以下来自文章 一文搞懂Javascript中的函数柯里化(currying)
用处:延迟计算、参数复用、动态生成函数
16. this指向情况有哪些?
(1) 普通函数,取决于它的调用方
(2) 箭头函数,取决于它定义时绑定的作用域中 this 的指向,具体一点,如果定义时,处于全局作用域,那么就指向 window,定义时,处于某一函数作用域,那么就指向该函数被执行时的 this,也就是指向该函数的调用方.
(3)call, apply, bind 调用时,可以通过第一个参数指定普通函数 this 的指向。但是如果原函数是箭头函数,那么修改将不会生效
17. 字符串常见方法
☆☆☆ 18. JS常见数组题:
(1)map和forEach区别
两者都可遍历数组,但是map可以返回一个数组,forEach不返回值。
// map ele不可省略,index可省略
const newArr = arr.map(function (ele, index) {
// console.log(ele); //数组元素
// console.log(index); //索引号
return ele + "颜色";
});
console.log(newArr);
//(4) ['red颜色', 'green颜色', 'pink颜色', 'blue颜色']
// forEach item不可省略,index可省略
const arr = ["red", "green", "blue"];
const res = arr.forEach(function (item, index) {
console.log(item); //数组元素
console.log(index); //索引号
});
console.log(res); //undefined
(2)创建数组有哪几种方式
<script>
// 创建数组
// 1. 字面量创建
let arr1 = [1, 2, 3];
console.log(arr1);
// 2. 使用Array构造函数
let arr2 = new Array(1, 2, 3);
console.log(arr2);
// 3.Array.of方法
let arr3 = Array.of(1, 2, 3);
console.log(arr3);
// 4. Array.from方法
let arr4 = Array.from([1, 2, 3]);
console.log(arr4);
let strArr = Array.from("string");
console.log(strArr);
// ['s', 't', 'r', 'i', 'n', 'g']
</script>
(3)遍历数组有哪几种方式
<script>
// 数组遍历
let arr = [1, 2, 3, 4];
// 1. for循环
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 2. forEach方法
arr.forEach((item) => console.log(item));
// 3. map方法 新数组
arr.map((item) => console.log(item));
// 4. reduce方法
arr.reduce((prev, current) => console.log(current), []);
// 5. for...in循环(不推荐用于数组,因为他会遍历数组的所有可枚举属性)
for (let index in arr) {
console.log(arr[index]);
}
// 6. for...of循环
for (let item of arr) {
console.log(item);
}
// 7. while循环
let i = 0;
while (i < arr.length) {
console.log(arr[i]);
i++;
}
</script>
(4)数组常见方法
arr.map
:处理(返回新数组)
arr.filter
: 筛选(返回新数组)
arr.every
: 每一项都要符合条件才行true/false
arr.some
: 有一项符合即可true/false
arr.fill
: 从某个位置开始替换(返回新数组)
arr.findIndex
: 返回第一个符合条件的索引值
arr.find
: 返回第一个符合条件的值,没有返回undefined
arr.indexOf()
: 返回数组中第一次出现给定元素的下标,如果不存在则返回 -1。indexOf(查找元素, 起始索引)
arr.includes()
: 用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
arr.reduce()
:返回累计器结果
(5)数组元素加前缀/后缀
<script>
// 数组元素加统一前缀/后缀
let arr = [1, 2, 3, 4];
// 1. forEach方法实现
let arr1 = [];
arr.forEach((item) => arr1.push(`排名${item}`));
console.log(arr1); //['排名1', '排名2', '排名3', '排名4']
// 2. map实现
let arr2 = arr.map((item) => `排名${item}`);
console.log(arr2);
// 3. for循环
let arr3 = [];
for (let i = 0; i < arr.length; i++) {
arr3.push(`排名${arr[i]}`);
}
console.log(arr3);
// 4. reduce方法,初始值是[]
let arr4 = arr.reduce((prev, current) => {
if (!prev.includes(current)) {
prev.push(`排名${current}`);
return prev;
}
}, []);
console.log(arr4);
// 5. Array.from()方法
let arr5 = Array.from(arr, (item) => `排名${item}`);
console.log(arr5);
</script>
(6)数组去重
<script>
// 数组去重
let arr = [1, 2, 3, 4, 3, 2];
// 1. 使用set方法,因为set元素不可重复
let arr1 = Array.from(new Set(arr));
console.log(arr1);
// 2. 使用filter和indexof
let arr2 = arr.filter(function (ele, index) {
if (arr.indexOf(ele) === index) {
console.log(`ele:${arr.indexOf(ele)} index:${index}`);
return ele;
}
});
console.log(arr2);
// 3. 使用reduce和includes,初始值是[]
let arr3 = arr.reduce(function (prev, current) {
if (!prev.includes(current)) {
prev.push(current);
}
return prev;
}, []);
console.log(arr3);
// 4. 使用forEach和includes
let arr4 = [];
arr.forEach(function (item, index) {
if (!arr4.includes(item)) {
arr4.push(item);
}
});
console.log(arr4);
// 5. 利用obj键唯一实现
let arr5 = [];
let obj = {};
arr.forEach((item) => {
if (!obj[item]) {
obj[item] = true;
arr5.push(item);
}
});
console.log(arr5);
</script>
(7)数组类型判断方法
<script>
// 判断是不是数组
let arr = [1, 2, 3];
// 1. Array.isArray()
console.log(Array.isArray(arr)); //true
// 2. instanceof方法
console.log(arr instanceof Array); //true
// 3. 使用Object.prototype.toString.call()
let judge = Object.prototype.toString;
console.log(judge.call(arr)); //[object Array]
</script>