目录
12、bind、call、apply的区别(改变普通函数的this指向)
1、JS的数据类型有哪八种
基本(简单)数据类型:Number、String、Boolean、BigInt(ES6新类型)、Symbol、Null、Undefined
特点:
直接存储在栈(由操作系统自动分配释放)中的简单数据段,占据空间小,属于被频繁使用的数据,赋值时直接生成相同的值,地址不同。
Symbol:
ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以作为object的key,为对象添加唯一属性。
let key = Symbol('key');
let obj = { [key]: 'symbol'};
let keyArray = Object.getOwnPropertySymbols(obj); // 返回一个数组[Symbol('key')]
obj[keyArray[0]] // 'symbol'
引用(复杂)数据类型:Object(普通对象,数组,正则,日期,Math数学函数)
特点:
存储在堆(一般由程序员分配释放)内存中,占据空间大,栈中存放指向堆内存的地址,所以赋值时相当于赋指针。
2、如何判断一个对象为空对象
let obj = {};
// Reflect.ownKeys方法返回一个由目标对象自身的属性组成的数组
// =Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
console.log(Reflect.ownKeys(obj).length === 0);
// undefined、函数以及 symbol 会出错
console.log(JSON.stringify(obj) === "{}");
// 会返回对象自身可枚举属性组成的数组,而不会遍历原型上的属性,Symbol不可用
console.log(Object.keys(obj).length === 0);
// 可以得到对象自身的所有属性名组成的数组,Symbol不可用
console.log(Object.getOwnPropertyNames(obj).length == 0);
// for in 循环判断,Symbol不可用
function isEmptyObj(obj) {
for (let item in obj) {
return true;
}
return false;
}
console.log("对象是否为空:", isEmptyObj({}));
// 不可枚举属性
Object.defineProperty(obj, "a", {
value: 1,
enumerable: false,
});
3、浅拷贝深拷贝
(1)概念区别
浅拷贝:会在栈中开辟另一块空间,并将被拷贝对象的栈内存数据完全拷贝到该块空间中,即基本数据类型的值会被完全拷贝,而引用类型的值则是拷贝了 “指向堆内存的地址” 。
深拷贝:深拷贝是拷贝多层,每一级别的数据都会拷贝出来。
(2)实现
浅拷贝:
// 该方法可以用于JS 对象的合并,不能拷贝对象的继承和不可枚举属性
Object.assign(newObj, obj)
// 扩展运算符,缺陷同上
let newObj = { …obj };
// 数组是被浅拷贝复制了,但如果里面有其他元素比如对象,那就是复制指针了
let newObj = obj.concat()
// 同上
let newObj = obj.slice(begin, end);
深拷贝:
// 函数、undefined、symbol不适用,原型链无法拷贝
let b = JSON.parse(JSON.stringify(a))
// 递归复制
function deepCopyTwo(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj == 'object') {
for (const key in obj) {
//判断obj子元素是否为对象,如果是,递归复制
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepCopyTwo(obj[key]);
} else {
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
return objClone;
}
// lodash是一个js原生库
let _ = require('lodash');
let newObj = _.cloneDeep(obj);
4、dom
DOM是JS操作网页的接口,全称为“文档对象模型”(Document Object Model)。浏览器会根据DOM模型,将结构化文档(比如HTML和XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口,从而可以用脚本进行各种操作(比如增删内容)。
var x1=document.getElementById("main"); // 通过 id 查找 HTML 元素
var x2=x1.getElementsByTagName("p"); // 通过标签名查找 HTML 元素集合
var myNodelist = document.querySelectorAll("p").length; // 获取 <p> 元素的集合长度
var x3=document.getElementsByClassName(""); // 通过类名找到 HTML 元素
document.write(""); // 向 HTML 输出流写内容(覆盖文档)
document.getElementById("").innerHTML=""; // 改变 HTML 内容
document.getElementById("").style.color="blue"; // 改变 HTML 样式
<body οnlοad="" οnunlοad=""> // 进入或离开页面时被触发
- onchange:当用户改变输入字段的内容时触发,常结合对输入字段的验证来使用。
- onmouseover 和 onmouseout :在用户的鼠标移至 HTML 元素上方或移出元素时触发。
- 当点击鼠标按钮时,会触发onmousedown事件;当释放鼠标按钮时,会触发onmouseup事件;当完成鼠标点击时,会触发onclick事件
5、重排、重绘
重排:
浏览器渲染页面是基于流式布局的,对某一个DOM节点信息进行修改时,需要对该DOM结构进行重新计算。该DOM结构的修改会决定周边DOM结构的更改范围,主要分为全局范围(整个渲染树)和局部范围(渲染树的某部分)。
浏览器会维护一个重排操作队列,等队列中的操作达到了一定的数量或者一定的时间间隔时,浏览器才会去刷新一次队列,进行真正的重排操作。
常见操作:
页面首次渲染。
浏览器窗口大小发生改变。
元素大小、形状、位置改变。
获取一些样式属性和函数。
重绘:
重绘只是改变元素在页面中的样式,而不会引起元素在文档流中位置的改变,例如字体颜色。重排一定会引起重绘的操作,而重绘不一定会引起重排的操作。
6、Promise
Promise 是一个 ES6 提供的类,目的是更加优雅地书写复杂的异步任务,能够解决回调地狱问题,比如下面这种多次调用异步任务造成的嵌套 "函数瀑布"。
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000);
现在我们用 Promise 来实现同样的功能:
// 新建,然后,然后。。。
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve(); // 异步操作执行成功后的回调函数
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
reject('执行失败'); // 异步操作执行失败后的回调函数
throw "err"; // return 是不能中断的,可以通过 throw 来跳转至 catch 实现中断。
}, 3000);
}).catch(function (err) { // catch 块只会执行第一个
console.log(err);
}).finally(function () { // finally 与 then 一样会按顺序执行
console.log("End");
});
简化一下就成了:
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}
print(1000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});
Promise 不是一种将异步转换为同步的方法,只不过是一种更良好的编程风格。
为什么这样说呢?因为更简单的写法如下:
async function asyncFunc() {
// 在异步函数中说“啊,等等”,于是它们排起队
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
try {
await new Promise(function (resolve, reject) {
throw "Some error"; // 或者 reject("Some error")
});
} catch (err) {
console.log(err);
}
}
asyncFunc();
Promise 的三种状态:
pending
(等待态)、fulfiled
(成功态)、rejected
(失败态)特点:状态改变之后就不会再变;承诺结果不会受外部影响。
缺点:
1)无法取消Promise,一旦新建它就会立即执行,无法中途取消,执行状态不可逆;
2)如果不设置回调函数,Promise内部抛出的错误,不会反映到外部;
3)当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成。Promise 本身是同步的,Promise 的回调
then、
catch
是异步。then()状态变化触发,catch()发生错误触发,finally() 最后必然执行的操作。
Promise的实例方法拓展
- p = Promise.all([ ]) :只要数组中有一个状态为
rejected,
p 的状态就变成rejected,
第一个被reject
的实例,返回值会传递给p
的回调函数。- Promise.race([ ]):数组中谁先改变状态, p 也就会跟着改变状态。率先改变的会将返回值传递给 p 的回调函数。
- Promise.allSettled():用来确定一组异步操作是否都结束了(不管成功或失败)。
- Promise.any():ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。
7、实现类new功能函数
function myNew(Fn, ...args){
// 创建一个空对象,作为此函数返回的对象实例
const obj = {}
// 将创建的空对象原型指向构造函数的prototype属性
obj.__proto__ = Fn.prototype
// 同时将这个空对象赋值给构造函数内部的this
const res = Fn.call(obj, ...args)
// 返回初始创建的对象或构造函数的显式返回值
return ['object', 'function'].includes(typeof res) ? res : obj
}
8、原型链
- 每个函数都有一个 prototype 指向自己的原型对象,即显示原型。
- 每个通过函数创建出来的实例对象都有一个__proto__,可称为隐式原型,这个属性指向当前对象的构造函数的原型对象。
- 对于原型对象来说,它有个constructor属性,指向它的构造函数。
- 原型对象的构造函数是Object(),而Object.prototype没有上一层的原型对象。
- 函数对象的构造函数是Function() ,而 Function()本身也是函数,所以Function() 是自己的实例。
9、JS如何实现继承(原型链扩展)
(1)使用class语法,用extends进行继承
class Car {
constructor(brand) {
this.brand = brand;
}
showBrand() {
console.log("the brand of car :>> ", this.brand);
}
}
// 其实跟java很像吧阿巴阿巴
class ElectricCar extends Car {
// 啊哈哈哈重载了
constructor(brand, duration) {
super(brand);
this.duration = duration;
}
showDuration() {
console.log(`duration of this ${this.brand} ElectricCar :>> `, this.duration);
}
}
// 给原型对象Car{}加个方法
ElectricCar.prototype.showOriginator = function (originator) {
console.log(`originator of this ElectricCar :>> `, originator);
};
const tesla = new ElectricCar("tesla", "600km");
tesla.showBrand(); // the brand of car :>> tesla
tesla.showDuration(); // duration of this tesla ElectricCar :>> 600km
// instanceof用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
console.log("tesla instanceof Car :>> ", tesla instanceof Car); // tesla instanceof Car :>> true
console.log("tesla instanceof ElectricCar :>> ", tesla instanceof ElectricCar); // tesla instanceof ElectricCar :>> true
console.log("tesla.__proto__ :>> ", tesla.__proto__); // tesla.__proto__ :>> Car {}
console.log("ElectricCar.prototype === tesla.__proto__ :>> ", ElectricCar.prototype === tesla.__proto__); // ElectricCar.prototype === tesla.__proto__ :>> true
tesla.showOriginator("Mask"); // originator of this ElectricCar :>> Mask
(2)直接改变对象的__proto__指向
const bydCar = {
brand: "比亚迪",
duration: "666km",
};
bydCar.__proto__ = ElectricCar.prototype;
bydCar.showBrand(); //the brand of car :>> 比亚迪
10、ES6新特性
(1)let和const
let、const、var的区别:
- let和const声明块级作用域({});var声明函数作用域(fn(){}),所以let和const只能在块级作用域里访问,如果使用if(){let a=10},在if外面console会报错找不到对象。
- let和const没有变量提升,var有(比如我在console之后才用var定义变量,那么console会输出undefined,但是用let和const定义就会报错找不到变量)。
- let和const不能重复声明,var可以。
- const定义常量,不能修改,let可以。
var 和 let 可以不设置初始值,const 声明变量必须设置初始值。
(2)proxy(vue3响应式)
(3)箭头函数
箭头函数和普通函数的区别:
- 箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。
- 箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。
(4)字符串新方法
includes()
判断字符串是否包含参数字符串,返回boolean值。startsWith() / endsWith()
,判断字符串是否以参数字符串开头或结尾。返回boolean值。这两个方法可以有第二个参数,一个数字,表示开始查找的位置。repeat()
方法按指定次数复制返回一个新的字符串。padStart()/padEnd()
,用参数字符串按给定长度从前面或后面补全字符串,返回新字符串。arr.filter(function (item) {return item > 2}),返回过滤值。
every(function (item,index,arr) {}),是否满足所有条件,返回布尔值。
some(function (item,index,arr) {}) ,数组中有没有满足条件的,返回布尔值。
find(function (item,index,arr) {}),返回数组中满足条件的第一个数据。
(5)解构
// 数组解构
let [a,b,c] = [1,2,3];
console.log(a,b,c); //1,2,3
// 对象解构
let obj = {
name: "ren",
age: 12,
sex: "male"
};
let { name, age, sex } = obj;
console.log(name, age, sex); //'ren' 12 'male'
(6)map和foreach的区别
Map对象用于保存键值对,forEach用来循环遍历数组
// 数组的forEach和map方法有哪些区别
const arr = [1, 2, 3, 4, 5, 6];
// forEach是对数组的每一个元素执行一次给定的函数。
arr.forEach(x => {
x = x + 1;
console.log("x :>> ", x);
});
// map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回的值。
const mapArr = arr.map(x => {
x = x * 2;
return x;
});
console.log("mapArr :>> ", mapArr);
(7) 数组方法
改变原数组的方法:
- pop():删除尾部元素,返回被删元素。
- push():将一个元素或多个元素添加到数组末尾,返回新长度。
- shift():删除头部元素,返回被删元素。
- unshift():将一个或多个元素添加到数组的开头,返回新长度。
- splice():截取数组arr.splice(1,2);删除并插入数据:数组名.splice(开始索引,多少个,你要插入的数据),返回被删元素。
- reverse(): 反转数组。
- sort():升序:arr.sort(function(a,b){return(a-b)});降序:arr.sort(function(a,b){return(b-a)})
不改变原数组的方法:
- concat():拼接数组。
- join('连接符'):数组拼接成字符串。
- slice(开始下标,结束下标):截取一段,返回截取值(包含开始下标不包含结束下标)。
indexOf:arr.indexOf(10):从左检查数组中有没有这个数值,返回该数据第一次出现的下标;arr.indexOf(10,n):返回10第n次出现的下标。
lastIndexOf():同上相反
(8)数组去重
Set对象和Map对象类似,但它存储不是键值对。类似数组,但它的每个元素都是唯一的。
// 利用set给数组去重
var list = [
1,2,1,{name:1},{name:1},null, NaN,0,0,{},'','',[1],[1],null, undefined,
false,9, undefined,'true','false','true'
]
console.log(Array.from(new Set(list)))
console.log([...new Set(list)])
11、闭包
闭包让开发者可以从内部函数访问外部函数的作用域。如果一个函数访问了此函数的父级及父级以上的作用域变量,就可以称这个函数是一个闭包。常见的闭包使用有两种场景:一种是函数作为参数被传递;一种是函数作为返回值被返回。
(1)闭包实现累加器
function add() {
var count = 0;
function demo() {
count++;
console.log(count);
}
return demo;
}
var counter = add();
counter();
counter();
counter();
(2)易出现的bug
var elements = document.getElementsByTagName('li');
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = function () {
console.log(i); // 会输出44444……
alert(i);
};
}
// 每个li标签的onclick事件执行时,本身onclick绑定的function的作用域中没有变量i
// i为undefined,则解析引擎会寻找父级作用域,发现父级作用域中有i
// 且for循环绑定事件结束后,i已经赋值为4
// 所以每个li标签的onclick事件执行时,alert的都是父作用域中的i,也就是4。
// 这是作用域的问题。这里onclick事件绑定的函数形成闭包,但引用的变量i是全局的。
for (let i = 0; i < elements.length; i++) { // 这样修改
elements[i].onclick = function () {
return (function (n) {
console.log(n);
alert(n);
})(i);
};
}
(3)内存泄漏
因为闭包就是能够访问外部函数变量的一个函数,而函数是必须保存在内存中的对象,所以位于函数执行上下文中的所有变量也需要保存在内存中,这样就不会被回收,如果一旦循环引用或创建闭包,就会占据大量内存,可能会引起内存泄漏。
解决方法:在退出函数之前,将不使用的局部变量全部删除,可以使变量赋值为 null。
12、如何获取对象类型
- Object.prototype.toString.call(obj)
- constructor(Object.constructor())
- typeof 缺点:判断引用类型时,比如typeof {}全返回object
- [] instanceof Array 用于检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上。缺点:不是通过实例化得到的对象就无法判断,比如console.log('哈哈' instanceof String) 输出 false
13、如何判断 this 指向
14、bind、call、apply的区别(改变普通函数的this指向)
参数不同:
call(this,arg1,arg2…)
apply(this,[arg1,arg2,…])
bind(this,[arg1,arg2,..])
函数执行不同:call,apply方法之后调用后,函数立即执行。
bind方法调用后,返回了一个改变this后的函数,不会立即执行。
被bind绑定过this的函数,this不会再被改变。
常见应用:
1.判断数据类型:Object.prototype.toString.call(null) //[object Null ]2.获取函数最大值和最小值:Math.max.apply(Math,[1,2,3])
3.伪数组转真数组:Array.prototype.slice.call(arguments)