JS基础题汇总

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后,只会执行一次
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值