JS基础题汇总
【参考掘金神三元大神的"原生js灵魂之问",JS高级程序设计、web前端面试-面试官系列等】
1.对象作为函数参数传递
说出如下代码的运行结果,解释原因
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
首先我们追溯到变量:变量包含两种不同类型的数据:原始值和引用值。原始值就是最简单的数据,引用值则是多个值构成的对象。在把一个值赋给变量时,保存原始值的变量是按值访问的,我们操作的就是存储在变量中的实际值。引用值是保存在内存中的对象,JS不允许直接访问内存位置,在操作对象时,实际上操作的是该对象的引用;除了存储方式不同之外,原始值和引用值在通过变量复制时也有所不同,原始值会被复制到新的变量的位置,两者存储的值相同,但是对于彼此是完全独立的。在把引用值从一个变量赋给另一个变量时,复制的值是一个指针,它指向存储在堆内存中的对象,因此一个对象上面的变化会在另一个对象上反映出来。也就是说,两个对象的实例指向同一个对象。
接下来说到传递参数,ECMAScript中所有函数的参数都是按值传递的,以上述代码为例:p1对象作为参数被传给test()方法中,并复制到参数person中,在函数内部,p1和person都指向同一个引用,它们指向的对象保存在全局作用域上的堆内存上,当对person.age进行改变时,p1.age自然会改变。p2变量则是保存的是person被重写的指针。
结果:
p1:{name: “fyq”, age: 26}
p2:{name: “hzj”, age: 18}
2.typeof与instanceof
- 手动实现instanceof功能
instanceof主要作用就是判断一个实例是否属于某种类型,实现原理在于右边变量的prototype在左边变量的原型链上即可,在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败就会返回false.
function new_instanceof(leftValue,rightValue) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof leftValue !== 'object' || leftValue === null) return false;
let left = leftValue._proto_;
let right = rightValue.prototype;
while(true) {
if(left=null) { return false;}
if(left=right) { return true;}
left=left._proto_
}
}
-
二者区别
typeof与instanceof都是判断数据类型的方法,区别如下:typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值 instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型 而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断 可以看到,上述两种方法都有弊端,并不能满足所有场景的需求
如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串
代码如下:
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
3.数组转换之[]==![]结果是什么?
任意对象转换成布尔值都是true,但是在比较时,遇到“!”优先将值转换成布尔值,不是遇到”!”优先转换成字符串,[]直接转换成布尔值是true,所以![]转换成布尔值为false,[]转换成字符串是“”,再转换成布尔值是false,最后都转成数字0。0==0,结果为true.
4.对象转换为原始值
首先判断对象是否有Symbol.toPrimitive(hint)方法,若有则执行该方法,没有执行以下步骤:
对象转换为字符串:如果对象有toString()这个方法,JS会将这个值转换为字符串,并返回一个字符串结果。如果没有toString()这个方法,就会调用valueof()这个方法。
对象转换成默认类型或者数字类型时,则优先执行valueof()方法,若没有valueof()方法,但是定义了toString()方法,则会执行toString()方法。
举例:
如何让if(a == 1&&a == 2)条件成立
var a={
value:0,
valueOf:function() {
this.value++;
return this.value;
}
};
console.log(a==1&&a==2);
当两个操作符用两个等于号表示,这两个操作符会先进行类型转换,通常成为强制类型转换,如果一个操作符是对象,另一个不是,则调用对象的valueOf()方法取得其原始值。
5.NaN表示非数字值,特殊之处:它和任何值都不相等,包括自身。判断NaN的方法:x!=x返回true
6.jQuery siblings(),next() ,find() 方法
1.siblings() 方法返回被选元素的所有同胞元素。
下面的例子返回 < h2 > 的所有同胞元素:
实例
$(document).ready(function(){
$("h2").siblings();
});
也可以使用可选参数来过滤对同胞元素的搜索。
下面的例子返回属于 < h2 > 的同胞元素的所有 < p > 元素:
实例
$(document).ready(function(){
$("h2").siblings("p");
});
2.jQuery next() 方法
next() 方法返回被选元素的下一个同胞元素。该方法只返回一个元素。
下面的例子返回 < h2 > 的下一个同胞元素:
实例
$(document).ready(function(){
$("h2").next();
});
3.jQuery find() 方法
find() 方法返回被选元素的后代元素,一路向下直到最后一个后代。
下面的例子返回属于 < div > 后代的所有 < span > 元素:
实例
$(document).ready(function(){
$("div").find("span");
});
7.JS的变量提升和函数提升
指的是var声明变量或用function函数名(){}声明的,会在js预解析阶段提升到顶端,其次,函数提升优先级高于变量提升
console.log(foo);
var foo = 1 //变量提升
console.log(foo)
foo()
function foo(){ //函数提升
console.log('函数')
}
等价于:
function foo(){ //提到顶端
console.log('函数')
}
var foo
console.log(foo) //输出foo这个函数,因为上面foo没有被赋值,foo还是原来的值
foo = 1; //赋值不会提升,赋值后 foo就不再是函数类型了,而是number类型
console.log(foo) //输出1
foo() //这里会报错,因为foo不是函数了
8.闭包
闭包的概念:指那些引用了另一个函数作用域中变量的函数。
理解作用域链对闭包理解非常重要:在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链,然后用arguments和其他命名参数来初始化这个函数的活动对象。活动对象被推入作用域链的前端。外部函数的活动对象是内部函数作用域链上的第二个对象,这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。在函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象。它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只有在函数执行期间存在。
function creatCompare(propertyName) {
return function (obj1,obj2) {
let value1 = obj1[propertyName];
let value2 = obj2[propertyName];
if(value1<value2) {return -1;}
else if(value1>value2) {return 1;}
else {return 0;}
};
}
let compare = creatCompare('name');
let result = compare({name:'Nicholas'},{name:'Matt'});
在这个例子中,函数内部返回一个函数,被返回的函数中调用着外部函数的变量propertyName,在这个内部函数中被返回并在其他地方使用后,它仍然引用着这个变量,这个函数会把其包含函数的活动对象添加到自己的作用域链中,在creatCompare()函数中,匿名函数的作用域链上实际包含着creatCompare()的活动对象,具体如下:
来自JS高级程序设计P311
闭包的特点:
让外部访问函数内部变量成为可能;
可以避免使用全局变量 ,防止全局变量的污染;
可以让局部变量常驻于内存中;
会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
应用场景:
1.埋点(网站分析的一种常用的数据采集方法)。在定时器,事件监听,AJAX请求,跨窗口通信,Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
function count(){
var num = 0;
return function() {
return ++num;
}
}
var getNum = count();
var getNewNum = count();
document.querySelectorAll('button')[0].onclick = function (){
console.log('点击加入购物车的次数:' + getNum());
}
document.querySelectorAll('button')[1].onclick = function (){
console.log('点击付款次数:' + getNewNum());
}
2.立即执行函数创建了闭包,保存了全局作用域和当前函数的作用域
3.函数作为参数传递
4.返回一个函数(上述举例)
关于闭包问题最经典的例子:
如何解决下面的循环输出问题?
for(var i = 1;i <= 5;i++) {
setTimeout(function timer(){
console.log(i)
},0)
}
为什么全部输出为6,如果理解了刚开始的那个闭包机制,不难看出,因为setTimeout是宏任务,JS中单线程eventLoop机制,在主线程同步任务执行完才去执行宏任务,因此循环结束后全局变量中有一个i,值为6。在执行宏任务时候,在当前回调函数的作用域中没有发现i,在作用域链中上一层去寻找,找到了i,所有依次打印出来的都是6.
如何解决?
1.利用立即执行函数,每次for循环时,把此时的i变量传递到定时器中
for(i = 1;i <=5;i++) {
(function (j){
setTimeout(function timer(){
console.log(j)
},0)
})(i)
}
2.给定时器传入第三个参数,作为timer函数的第一个函数参数
for(i = 1;i <=5;i++) {}
setTimeout(function timer(j){
console.log(j)
},0,i)
}
3.使用ES6中的let,es6新增了块级作用域,所声明的变量只在当前块内有效。使用let后作用域链不复存在
for(let i = 1;i <=5;i++) {
setTimeout(function timer(){
console.log(i)
},0)
}
9.变量的命名:
(1)第一个字符必须是一个字母、下划线(_)或一个美元符号($);其他字符可以是字母、下划线、美元符号或数字。
(2)变量名不能包括空格、加、减等符号;
(3)不能使用JS中关键字作为变量名,如int,new等;
(4)JS的变量名严格区分大小写;
10.继承
实现继承是ECMAScript唯一支持的继承方式,而这主要是通过原型链实现的
第一种:借助原型链
当原型中包含引用值的时候,原型中的引用值会在所有实例间共享,在使用原型继承的时候,原型实际上变成了另一个原型的实例`
function Parent1() {
this.play = [1, 2, 3]
}
function Child1() {
}
Child1.prototype = new Parent1();
var s1 = new Child1();
var s2 = new Child1();
s1.play.push(4);
console.log(s1.play, s2.play);//[1,2,3,4],[1,2,3,4]
Parent1所有的实例拥有play这个属性,当被Child1通过原型链继承了之后,Child1.prototype变成了Parent1的一个实例,所有也获得自己的play属性,结果是,Child1上的所有实例都会共享这个属性。所以基本上原型链继承基本不会被单独使用。
第二种:盗用构造函数
function Parent2(){
this.play = [1, 2, 3]
}
function Child2(){
Parent2.call(this);
}
Child2.prototype = new Parent2();
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);//[1,2,3,4],[1,2,3]
为了解决原型包含引用值导致的继承问题,在子类构造函数中调用父类的构造函数,使用call()或apply()方法,结果是每个实例都会有自己的play属性。但是子类不能访问父类原型上定义的方法。盗用构造函数也基本不能单独使用。
第三种:组合继承
function Parent3 (name) {
this.name = name;
this.play = [1, 2, 3];
}
Parent3.prototype.sayName = function(){
console.log(this.name);
}
function Child3() {
Parent3.call(this,name);
}
Child3.prototype = new Parent3();
var s3 = new Child3("a");
var s4 = new Child3("b");
s3.play.push(4);
console.log(s3.play, s4.play);
console.log(s3.sayName(), s4.sayName();
组合继承存在效率问题,就是父类构造函数始终会被调用两次,一次是在创建子类原型时调用,第二次在子类构造函数中调用。
第四种:寄生组合
function Parent4(name) {
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
}
Child4.prototype = Object.create(Parent4.prototype);
Child4.prototype.constructor = Child4;
var s3 = new Child4();
s3.play.push(4);
console.log(s3);
Object.create()方式本质上克隆了两个Parent4,只调用了一次Parent4构造函数, Child4.prototype.constructor = Child4也使实例对象重新指向Child4 解决由重写原型导致默认onstructor丢失的问题。寄生式组合继承可以算是引用类型继承的最佳模式。
这里提一下,es6中的extends关键字直接继承js的继承
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)//super()相当于Person.call(this)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
extends实际采用的也是寄生组合继承方式
继承本身存在的问题
无法决定继承哪些属性,所有的属性都要继承,父子的代码耦合性太高。解决方案:面向组合的设计方式。
11、*
在js里面添加的属性名使用驼峰法,在css里面使用连接线 除了id和query 其他返回的都是节点列表
结果为:document.getElementsByClassName(“test”)[0].style.backgroundColor=“red”;
12、*与其他 IEEE 754 表示浮点数的编程语言一样,JavaScript 的 number 存在精度问题,比如 0.2 + 0.4 的结果是 0.6000000000000001。以下选项中,能得到 0.6 的是?
解析:
13、*
解析:答案为“error”
在 JS 里,声明函数只有 2 种方法:
第 1 种: function foo(){…} (函数声明)
第 2 种: var foo = function(){…} (等号后面必须是匿名函数,这句实质是函数表达式)
除此之外,类似于 var foo = function bar(){…} 这样的东西统一按 2 方法处理,即在函数外部无法通过 bar 访问到函数,因为这已经变成了一个表达式。
但为什么不是 “undefined”?
这里如果求 typeof g ,会返回 undefined,但求的是 g(),所以会去先去调用函数 g,这里就会直接抛出异常,所以是 Error。
14、*NOSCRIPT标签是做什么用的?
用来定义在脚本未被执行时的替代内容:
<body>
...
...
<script type="text/javascript">
<!--
document.write("Hello World!")
//-->
</script><noscript>Your browser does not support JavaScript!</noscript>...
...
</body>
15、undefined值是通过null派生出来的,= = 时它会自动转化为null,所以返回true。不过如果用严格比较符 ===,不发生转化,将返回false。
16、数组的扁平化
需求:多维数组转换成一维数组
假设有个名为 flatten 的函数可以做到数组扁平化, 效果就会如下:
var arr = [1, [2, [3, 4]]];
console.log(flatten(arr)) // [1, 2, 3, 4]
第一种:递归
let result = [];
function flatten(arr) {
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
flatten(arr[i]);
} else {
result.push(arr[i]);
}
}
return result;
arr = [1, [2, 3],[4, 5, 6]];
console.log(flatten(arr));
第二种 :toString(),split(’,’),map(callback)
let arr = [1, [2, 3], [4, 5, 6]];
//toString()将数组装换成扁平化字符串
//split(',')将字符串转换成以,分割的字符串数组
//map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。map() 方法按照原始数组元素顺序依次处理元素。
function flatten(arr) {
return arr.toString().split(',').map((item) => {
return +item;//快速将字符串转换成数字
})
}
console.log(flatten(arr));
第三种:reduce
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
}, [])
}
console.log(flatten(arr));
第四种:扩展运算符
let arr = [1, [2, [3, 4]]];
let result = [];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr;
};
console.log(flatten(arr));
第五种:ES6 flat()
let arr = [1, [2, [3, 4]]];
ary = arr.flat(Infinity)
console.log(ary);
17、Javascript定时器中的this指向
var name = 'my name is window';
var obj = {
name: 'my name is obj',
fn: function () {
var timer = null;
clearInterval(timer);
timer = setInterval(function () {
console.log(this.name); //my name is window
}, 1000)
}
}
obj.fn()
在这里,从this.name可以看出this的指向是window。
如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。但是平时很多场景下,都需要修改this的指向。这里总结了几种:
第一种:最常用的方法:在外部函数中将this存为一个变量,回调函数中使用该变量,而不是直接使用this。
var name = 'my name is window';
var obj = {
name: 'my name is obj',
fn: function () {
var that = this;
var timer = null;
clearInterval(timer);
timer = setInterval(function () {
console.log(that.name); //my name is obj
}, 1000)
}
}
在fn中加了var that = this; 回调函数中使用that代替this即可。这种方法最常见,使用也最广泛。
第二种:使用bind()方法(bind()为ES5的标准,低版本IE下有兼容问题,可以引入es5-shim.js解决)
bind()的作用类似call和apply,都是修改this指向。但是call和apply是修改this指向后函数会立即执行,而bind则是返回一个新的函数,它会创建一个与原来函数主体相同的新函数,新函数中的this指向传入的对象。
var name = 'my name is window';
var obj = {
name: 'my name is obj',
fn: function () {
var timer = null;
clearInterval(timer);
timer = setInterval(function () {
console.log(this.name); //my name is obj
}.bind(this), 1000)
}
}
在这里为什么不能用call和apply,是因为call和apply不是返回函数,而是立即执行函数,那么,就失去了定时器的作用。
第三种:使用es6的箭头函数:箭头函数的最大作用就是this指向。
var name = 'my name is window';
var obj = {
name: 'my name is obj',
fn: function () {
var timer = null;
clearInterval(timer);
timer = setInterval(() => {
console.log(this.name); //my name is obj
}, 1000)
}
}
箭头函数没有自己的this,它的this继承自外部函数的作用域。所以,在该例中,定时器回调函数中的this,是继承了fn的this。当然箭头函数也有兼容问题,要是兼容低版本ie,需要使用babel编译,并且引入es5-shim.js才可以。
引用于:博客园:白雪—wind https://www.cnblogs.com/443855539-wind/p/6480673.html
18、浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝,如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。以下实例说明浅拷贝拷贝的就是内存地址:
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function() {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, obj);
newObj.age = 20;
console.log(newObj);
console.log(obj);
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function() {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, obj);
newObj.names.name1 = 'a';
console.log(newObj);
console.log(obj);
浅拷贝的实现方式:
第一种:slice()
let arr = [1, [2, [3, 4]]];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr);//[1, [2, [3, 4]]]
console.log(newArr);//[100, [2, [3, 4]]]
第二种:手动实现
let arr = [1, [2, [3, 4]]];
const shallClone = function(target) {
if (typeof target === 'object' && target != null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
console.log(prop);//0 1
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
console.log(shallClone(arr));//1,[2, [3, 4]]
第三种:Object.assign()
const target = { a: 1, b: 2 };
const source = {b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
Object.assign()只适用于浅拷贝,假如源值是一个对象的引用,它仅仅会复制其引用值。它拷贝的是对象的属性的引用,而不是对象本身
const log = console.log;
function test() {
'use strict';
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
log(JSON.stringify(obj2));
// { a: 0, b: { c: 0}}
obj1.a = 1;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 0}}
log(JSON.stringify(obj2));
// { a: 0, b: { c: 0}}
obj2.a = 2;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 0}}
log(JSON.stringify(obj2));
// { a: 2, b: { c: 0}}
obj2.b.c = 3;
log(JSON.stringify(obj1));
// { a: 1, b: { c: 3}}
log(JSON.stringify(obj2));
// { a: 2, b: { c: 3}}
// Deep Clone
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
log(JSON.stringify(obj3));
// { a: 0, b: { c: 0}}
}
test();
*[详情见MDN]
第四种:concat浅拷贝数组
let arr = [1, [2, [3, 4]]];
let newArr = arr.concat();
newArr[0] = 100;
console.log(arr); //[1, [2, [3, 4]]]
console.log(newArr); //[100, [2, [3, 4]]]
第五种:…展开运算符
let arr = [1, [2, [3, 4]]];
let newArr = [...arr]
newArr[0] = 100;
console.log(arr); //[1, [2, [3, 4]]]
console.log(newArr); //[100, [2, [3, 4]]]
19、深拷贝
深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。以下实例说明深拷贝与浅拷贝的区别
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function() {
console.log('fx is a great girl')
}
}
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
//改变拷贝后的第二层属性值name1,obj内的值没有变,说明了深拷贝和浅拷贝的区别
deepClone(obj).names.name1 = 'a';
console.log(obj);
深拷贝的实现方式:
第一种: _.cloneDeep()
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
第二种:jquery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
第三种:JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
但是这种方式存在弊端,会忽略undefined、symbol、函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
第四种:开头的递归循环
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
引用于-- 面试官https://vue3js.cn/interview/JavaScript/copy.html#%E4%B8%89%E3%80%81%E6%B7%B1%E6%8B%B7%E8%B4%9D
20、V8执行一段JS代码的过程
- 首先通过词法解析和语法分析生成AST(抽象语法树)
- V8解释器将AST转换为字节码
- 由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码,以优化执行效率,这种字节码和解释器、编译器结合的技术称之为即时编译(JIT)
21、“ == ”操作符 隐式转换 等于操作符在比较中会先进行类型转换,再确定操作数是否相等
-
两个都为简单类型,字符串和布尔值都会转换成数值,再比较
-
简单类型与引用类型比较,对象转化成其原始类型的值,再比较
-
两个都为引用类型,则比较它们是否指向同一个对象
-
null 和 undefined 相等
-
存在 NaN 则返回 false
22、this中需要注意的地方
- 这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();
上述代码中,this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined
- 再列举一种特殊情况
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();
此时this指向的是window,这里的大家需要记住,this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window
- new过程遇到return一个对象,此时this指向为返回的对象
function fn()
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined
如果返回一个简单类型的时候,则this指向实例对象
function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn();
console.log(a.user); //xxx
注意的是null虽然也是对象,但是此时new仍然指向实例对象
function fn()
{
this.user = 'xxx';
return null;
}
var a = new fn();
console.log(a.user); //xxx
- this在标准函数和箭头函数中有不同的行为,在标准函数中,this引用的是函数当成方法调用的上下文对象,这时候通常称其为this值,定义在全局上下文中的函数引用了this这个对象,这个this到底引用哪个对象必须到函数被调用时才确定;在箭头函数中,this引用的是 定义 箭头函数的上下文
window.color = 'red';
let o = {
color: 'blue'
}
let sayColor = () => console.log(this.color);
sayColor(); //'red'
o.sayColor1 = sayColor;
o.sayColor1(); //'red'
函数名只是保存指针的变量,因此全局定义的sayColor()和o.sayColor1()是同一个函数,只不过是执行的上下文不同。
在事件回调或定时器回调中调用某个函数时,this值指向的并非想要的对象,此时将回调函数写成箭头函数就可以解决问题,这是因为箭头函数中的this会保留定义该函数时的上下文。
function King() {
this.royaltyName = 'Henry';
// this 引用 King 的实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
function Queen() {
this.royaltyName = 'Elizabeth';
// this 引用 window 对象
setTimeout(function() { console.log(this.royaltyName); }, 1000);
}
new King(); // Henry
new Queen(); // undefined
23、事件代理(事件委托)
把一个元素响应事件的函数委托到另一个元素,事件委托在冒泡阶段完成。当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层的绑定事件上,然后在外层元素上去执行函数。
如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的
// 获取目标元素
const lis = document.getElementsByTagName("li")
// 循环遍历绑定事件
for (let i = 0; i < lis.length; i++) {
lis[i].onclick = function(e){
console.log(e.target.innerHTML)
}
}
这时候就可以事件委托,把点击事件绑定在父级元素ul上面,然后执行事件的时候再去匹配目标元素
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判断是否匹配目标元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
还有一种场景是上述列表项并不多,我们给每个列表项都绑定了事件
但是如果用户能够随时动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的
举个例子:
下面html结构中,点击input可以动态添加元素
<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item 4</li>
</ul>
使用事件委托
const oBtn = document.getElementById("btn");
const oUl = document.getElementById("ul1");
var num = 4;
//事件委托,添加的子元素也有事件
oUl.onclick = function (ev) {
ev = ev || window.event;
const target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'li') {
console.log('the content is: ', target.innerHTML);
}
};
//添加新节点
oBtn.onclick = function () {
num++;
const oLi = document.createElement('li');
oLi.innerHTML = `item ${num}`;
oUl.appendChild(oLi);
};
适合事件委托的事件有:click,mousedown,mouseup,keydown,keyup,keypress
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
减少整个页面所需的内存,提升整体性能
动态绑定,减少重复工作
但是使用事件委托也是存在局限性:
focus、blur这些事件没有事件冒泡机制,所以无法进行委托绑定事件
mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件
引用:https://vue3js.cn/interview/JavaScript/event_agent.html#%E4%B8%89%E3%80%81%E6%80%BB%E7%BB%93
24、new操作符
在JavaScript中,new操作符用于创建一个给定构造函数的实例对象
new操作符主要做了以下工作:
- 创建一个新的对象obj
- 将对象与构造函数通过原型链连接起来
- 将构造函数中的this绑定到新建的对象obj上
- 根据构造函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,则实例对象的内容就会成为返回对象的内容
例如:
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
function Test(name) {
this.name = name
return 1
}
const t = new Test('xxx')
console.log(t.name) // 'xxx'
手写new操作符:
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj}
测试:
1、构造函数返回类型为原始值
function mynew(func, ...args) {
const obj = {}
obj.__proto__ = func.prototype
let result = func.apply(obj, args)
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name;
this.age = age;
return 1
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui
2、构造函数返回值为对象
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name;
this.age = age;
//构造函数返回类型为对象
return {
sex: 'woman'
}
}
Person.prototype.say = function() {
console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) //{sex: 'woman'}
p.say() // 报错 Uncaught TypeError: p.say is not a function
25、ajax原理和封装
1、 定义
异步的javascript和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页
2、原理
通过创建XmlHTTPRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JS来操作DOM而更新页面
3、实现过程:
- 创建Ajax的核心对象XMLHTTPRequest对象
- 通过对象的open()方法与服务器建立连接
- 构建所需的内容,并通过对象的send()方法发给服务器端
- 通过对象提供的onreadstatechange事件监听服务器端你的通信状态
- 接收并处理服务器端向客户端响应的数据结果
- 将结果更新到HTML页面中
4、封装
//封装一个ajax请求
function ajax(options) {
//创建XMLHttpRequest对象
const xhr = new XMLHttpRequest()
//初始化参数的内容
options = options || {}
options.type = (options.type || 'GET').toUpperCase()
options.dataType = options.dataType || 'json'
const params = options.data
//发送请求
if (options.type === 'GET') {
xhr.open('GET', options.url + '?' + params, true)
xhr.send(null)
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true)
xhr.send(params)
//接收请求
xhr.onreadystatechange = function () {
//XMLHttpRequest.readyState属性状态为4时表示整个请求过程已完毕
if (xhr.readyState === 4) {
let status = xhr.status
if (status >= 200 && status < 300) {
//XMLHttpRequest.responseText属性用于接收服务器端的响应结果
options.success && options.success(xhr.responseText, xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}}
使用方式如下:
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(text,xml){//请求成功后的回调函数
console.log(text)
},
fail: function(status){请求失败后的回调函数
console.log(status)
}
})
26、防抖和节流
1、定义
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(防抖)和debounce(节流)的方式来减少调用频率
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
2、防抖
// js防抖核心代码
function debounce (fn, delay) {
let timer = null
// 闭包
return () => {
// 如果当前正处在某个计时过程中,那么就清除此次计时开始下一次计时,
// 否则不清除,直接开始下一次计时,计时满delay毫秒后才会触发fn函数。
if (timer) {
clearTimeout(timer)
}
// 开始下一次计时
timer = setTimeout(fn, delay)
}
}
function handleScroll () {
console.log('滑动后一秒钟内不再滑动我就执行一次', Math.random())
}
window.addEventListener('scroll', debounce(handleScroll, 1000))
节流
// js节流核心代码
// 时间戳
function throttle1 (fn, delay) {
let prev = Date.now()
// 闭包
return () => {
let now = Date.now()
if (now - prev >= delay) {
fn()
prev = Date.now()
}
}
}
// 定时器
function throttle2 (fn, delay) {
let timer = null
// 闭包
return () => {
if (!timer) {
timer = setTimeout(() => {
fn()
timer = null
}, delay)
}
}
}
function handleScroll () {
console.log('滑动过程中我一秒钟才执行一次', Math.random())
}
window.addEventListener('scroll', throttle1(handleScroll, 1000))
// window.addEventListener('scroll', throttle2(handleScroll, 1000))
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
- 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次