2021届前端面试知识点(JavaScript)

2021届前端面试知识点(JavaScript)

1. ES6的新特性?

  1. 字符串扩展:

    • includes():返回布尔值,表示是否找到了参数字符串。

    • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。

    • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

  2. 解构表达式:

    数组解构:

    let arr = [1,2,3];
    const [x,y,z] = arr;   // x,y,z将与arr中的每个位置对应来取值
    // 然后打印
    console.log(x,y,z);
    

    对象解构:

    const person = {
        name:"jack",
        age:21,
        language: ['java','js','css']
    }
    // 解构表达式获取值
    const {name,age,language} = person;
    // 打印
    console.log(name);
    console.log(age);
    console.log(language);
    
  3. 函数优化:

    设置函数参数默认值:

    function add(a , b = 1) {
        return a + b;
    }
    // 传一个参数
    console.log(add(10));
    

    箭头函数:

    // 一个参数时:
    var print = function (obj) {
        console.log(obj);
    }
    // 简写为:
    var print2 = obj => console.log(obj);
    
    // 两个参数的情况:
    var sum = function (a,b) {
        return a + b;
    }
    // 简写为:
    var sum2 = (a,b) => a+b;
    
    // 代码不止一行,可以用{}括起来
    var sum3 = (a,b) => {
        return a + b;
    }
    
  4. map 和 reduce:

    **map():**接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。

    举例:有一个字符串数组,我们希望转为int数组:

    let arr = ['1','20','-5','3'];
    console.log(arr)
    arr = arr.map(s => parseInt(s));
    console.log(arr)
    

    **reduce():**接收一个函数(必须)和一个初始值(可选),该函数接收两个参数:第一个参数是上一次reduce处理的结果,第二个参数是数组中要处理的下一个元素。

    reduce()会从左到右依次把数组中的元素用reduce处理,并把处理的结果作为下次reduce的第一个参数。如果是第一次,会把前两个元素作为计算参数,或者把用户指定的初始值作为起始参数。

    const arr = [1,20,-5,3]
    console.log(arr.reduce((a,b) => a+b)) // 19
    
  5. promise:

    所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

    const p = new Promise(function (resolve, reject) {
        // 这里我们用定时任务模拟异步
        setTimeout(() => {
            const num = Math.random();
            // 随机返回成功或失败
            if (num < 0.5) {
                resolve("成功!num:" + num);  // 调用resolve,代表Promise将返回成功的结果
            } else {
                reject("出错了!num:" + num);  // 调用reject,代表Promise会返回失败结果
            }
        }, 300)
    })
    
    // 调用promise
    p.then(function (msg) {   // 异步执行成功后的回调
        console.log(msg);
    }).catch(function (msg) {   // 异步执行失败后的回调
        console.log(msg);
    })
    
  6. set和map:

    • Set本质与数组类似。不同在于Set中只能保存不同元素,如果元素相同会被忽略。
    • map,本质是与Object类似的结构。不同在于,Object强制规定key只能是字符串。而Map结构的key可以是任意对象。即:
      • object是 <string,object>集合
      • map是<object,object>集合
  7. 模块化:

    模块化就是把代码进行拆分,方便重复利用。模块功能主要由两个命令构成:export和import。

    export命令用于规定模块的对外接口,
    import命令用于导入其他模块提供的功能。
    

    export不仅可以导出对象,一切JS变量都可以导出。比如:基本类型变量、函数、数组、对象。当要导出多个值时,还可以简写。

  8. for…of:

    for…of 用于遍历一个迭代器,如数组:

    let letters = ['a', 'b', 'c'];
    letters.size = 3;
    for (let letter of letters) {
      console.log(letter);
    }
    // 结果: a, b, c
    
  9. class类:

    传统的JavaScript中只有对象,没有类的概念。它是基于原型的面向对象语言。原型对象特点就是将自身的属性共享给新对象。ES6 中支持 class 语法,不过,ES6的class不是新的对象继承模型,它只是原型链的语法糖表现形式。语法糖(Syntactic sugar),也译为糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。之所以叫「语法」糖,不只是因为加糖后的代码功能与加糖前保持一致,更重要的是,糖在不改变其所在位置的语法结构的前提下,实现了运行时的等价。可以简单理解为,加糖后的代码编译后跟加糖前一毛一样。之所以叫语法「糖」,是因为加糖后的代码写起来很爽,包括但不限于:代码更简洁流畅,代码更语义自然…

    在ES没有class类时,如果要生成一个对象实例,需要先定义一个构造函数,然后通过new操作符来完成。构造函数示例:

    //函数名和实例化构造名相同且大写(非强制,但这么写有助于区分构造函数和普通函数)
    function Person(name,age) {
        this.name = name;
        this.age=age;
    }
    Person.prototype.say = function(){
        return "我的名字叫" + this.name+"今年"+this.age+"岁了";
    }
    var obj=new Person("laotie",88);//通过构造函数创建对象,必须使用new 运算符
    console.log(obj.say());//我的名字叫laotie今年88岁了
    

    ES6引入了Class(类)这个概念,通过class关键字可以定义类。该关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言。如果将之前的代码改为ES6的写法就会是这个样子:

    class Person{//定义了一个名字为Person的类
        constructor(name,age){//constructor是一个构造方法,用来接收参数
            this.name = name;//this代表的是实例对象
            this.age=age;
        }
        say(){//这是一个类的方法,注意千万不要加上function
            return "我的名字叫" + this.name+"今年"+this.age+"岁了";
        }
    }
    var obj=new Person("laotie",88);
    console.log(obj.say());//我的名字叫laotie今年88岁了
    

2. ES6 的 class 和构造函数的区别

1.严格模式:
类和模块的内部,默认就是严格模式,所以不需要使用`use strict`指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

2. 不存在提升:
类不存在变量提升(hoist),这一点与 ES5 完全不同。


3. 方法默认是不可枚举的:
ES6 中的 class,它的方法(包括静态方法和实例方法)默认是不可枚举的,而构造函数默认是可枚举的。细想一下,这其实是个优化,让你在遍历时候,不需要再判断 hasOwnProperty 了

4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。

5. class 必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。

6. ES5 和 ES6 子类 this 生成顺序不同:
ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例。ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

7. ES6 class 可以继承静态方法,而构造函数不能。

3. JS的三大事件是什么?

鼠标事件:

click:单击
dblclick:双击
mousedown:鼠标按下
mouseup:鼠标抬起
mouseover:鼠标悬浮
mouseout:鼠标离开
mousemove:鼠标移动
mouseenter:鼠标进入
mouseleave:鼠标离开

键盘事件:

keydown:按键按下
keyup:按键抬起
keypress:按键按下抬起

HTML事件:

load:文档加载完成
select:被选中的时候
change:内容被改变
focus:得到光标
resize:窗口尺寸变化
scroll:滚动条移动

4. js判断是不是数组

  1. instanceof 判断
var arr = [1,23,4]
console.log(arr instanceof Array)  //true
  1. 原型链方法
var arr = [1,2,3]
console.log(arr.__proto__.constructor == Array) //true
console.log(arr.constructor == Array)  //true
  1. Object.prototype.toString.call()
let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true
  1. Array.isArray()
var a = [1,2,3];
console.log(typeof a);  //返回“object”
console.log(Array.isArray(a));  //true

5. js数组常用的操作方法,slice()、splice()、substring()、substr()区别?

  1. slice(start,end):可操作数组和字符串,不修改原数组。方法可从已有数组中返回选定的元素,返回一个新数组,包含从start(包含开始索引)到end(不包含结束索引)的数组元素。注意:该方法不会改变原数组,而是返回一个子数组,如果想删除数组中的一段元素,应该使用Array.splice()方法。

    • start参数:必须,规定从何处开始选取,如果为负数,规定从数组尾部算起的位置,-1是指最后一个元素。
    • end参数:可选(如果该参数没有指定,那么切分的数组包含从start到数组结束的所有元素,如果这个参数为负数,那么规定是从数组尾部开始算起的元素)。
  2. splice():只能操作数组,该方法向或者从数组中添加或者删除项目,返回被删除的项目。(该方法会改变原数组)。splice(start,deleteCount,item1,…itemX)

    • start参数:必须,整数,规定添加或者删除的位置,使用负数,从数组尾部规定位置。
    • deleteCount参数:必须,要删除的数量,如果为0,则不删除项目。
    • tem1,…itemX参数:可选,向数组添加的新项目。
  3. substring(start,end):只能操作字符串,返回从start位置开始到end位置的子串(不包含end),不改变原字符串。

  4. substr(start,length):只能操作字符串,返回从start位置开始length长度的子串,不改变原字符串。

进出栈操作:

push(value,...)         数组末端入栈操作
pop()                   数组末端出栈操作
unshift(value,...)      数组首端入栈操作
shift()                 数组首端出栈操作

连接数组:

join(bystr)          返回由bystr连接数组元素组成的字符串
toString()           返回由逗号连接数组元素组成的字符串
concat(value,...)    返回添加参数中元素后的数组

数组排序:

reverse()            返回反向的数组
sort()               返回排序后的数组

6. JavaScript 中的常用循环遍历(数组或对象)的方法

  1. for 循环:用于遍历数组

    let arr = [1,2,3];
    for (let i=0; i<arr.length; i++){
    	console.log(i,arr[i])
    }
    
  2. while 循环:用于遍历数组,和for效果相同,不过 for 循环可以把定义、条件判断、自增自减操作放到一个条件里执行,代码看起来方便一些。

    cars=["BMW","Volvo","Saab","Ford"];
    var i=0;
    while (cars[i]){
        console.log(cars[i] + "<br>")
        i++;
    };
    
  3. do while 循环:do while 循环是 while 循环的一个变体,它首先执行一次操作,然后才进行条件判断,是 true 的话再继续执行操作,是 false 的话循环结束。

    let i = 3;
    do{
        console.log(i)
        i--;
    }
    while(i>0)
    
  4. forEach 循环:forEach循环,循环数组中每一个元素并采取操作, 没有返回值, 可以不用知道数组长度。

    let arr = [1,2,3];
    arr.forEach(function(i,index){
        console.log(i,index)
    })
    
  5. map()方法:map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

    let arr = [1,2,3];
    let tt = arr.map(function(i){
        console.log(i)
        return i*2;
    })
    
  6. for of 循环:for of 循环是 Es6 中新增的语句,功能非常强大用来替代 for in 和 forEach,for-of循环不仅支持数组,还支持大多数类数组对象,它允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代(Iterable data)的数据结构,注意它的兼容性。

    let arr = ['name','age'];
    for(let i of arr){
        console.log(i)
    }
    

for…in…和for…of…的区别?

  • for…in…遍历的是键名,也就是索引;

  • for…of…可以遍历键值 。

7. js数组去重

(1)for循环(两次) + 新数组:

//方法一
var arr = [1,23,1,1,1,3,23,5,6,7,9,9,8,5];
function removeDuplicatedItem(arr) {
    for(var i = 0; i < arr.length-1; i++){
        for(var j = i+1; j < arr.length; j++){
            if(arr[i]==arr[j]){
                arr.splice(j,1);   //console.log(arr[j]);
                j--;
            }
        }
    }
    return arr;
}

arr2 = removeDuplicatedItem(arr);
console.log(arr);
console.log(arr2);

(2)for循环(一次) + 新数组:构建一个新的数组存放结果,for循环中每次从原数组中取出一个元素,用这个元素循环与结果数组对比,若结果数组中没有该元素,则存到结果数组中。

//方法二
var arr = [1,23,1,1,1,3,23,5,6,7,9,9,8,5];
function deleteDump(arr){
    let arr1 = [];
    arr1.push(arr[0]);
    for(var i=1;i<arr.length;i++){
        if(arr1.indexOf(arr[i]) === -1){
            arr1.push(arr[i]);
        }
    }
    return arr1
}
var res = deleteDump(arr);
console.log(res)

(3)for循环(一次) + 新数组 + 新对象 :利用空对象来记录新数组中已经存储过的元素

//方法三
var arr = [1,23,1,1,1,3,23,5,6,7,9,9,8,5];
var o={};
var new_arr=[];
for(var i=0;i<arr.length;i++){
	var k=arr[i];
    if(!o[k]){
        o[k]=true;
        new_arr.push(k);
	}
}
console.log(new_arr);

(4)for循环(一次) + sort()排序 + 新数组:原数组长度不变但被按字符串顺序排序,借助新数组 ,判断新数组中是否存在该元素如果不存在则将此元素添加到新数组中

//方法四
var arr = [1,23,1,1,1,3,23,5,6,7,9,9,8,5];
function removeRepEle(ar) {
    var ret = [],
    var end;  //临时变量用于对比重复元素
    ar.sort();   //将数重新组排序
    end = ar[0];
    ret.push(ar[0]);
    for (var i = 1; i < ar.length; i++) {
        if (ar[i] != end) {   //当前元素如果和临时元素不等则将此元素添加到新数组中
            ret.push(ar[i]);
            end = ar[i];
        }
    }
	return ret;
}

arr2 = removeRepEle(arr);
console.log(arr);//[ 1, 1, 1, 1, 23, 23, 3, 5, 5, 6, 7, 8, 9, 9 ]
console.log(arr2);//[ 1, 23, 3, 5, 6, 7, 8, 9 ]

8. js数组扁平化

给定数组:将多维数组转化为一维数组。

ary = [1, [2, [3, [4, 5]]], 6];
str = JSON.stringify(ary);
  1. 直接调用ES6的flat():

    ary = ary.flat(Infinity);  // flat里面的参数表示展开的深度,Infinity表示任意深度,也就是完全展开
    
  2. 正则:

    str = str.replace(/\[|\]/g, "");
    str = '[' + str + ']';
    ary = JSON.parse(str);
    console.log(ary);
    
  3. 递归:

    let res = [];
    let func = function(ary){
    	for(let i=0; i<ary.length; i++){
    		let item = ary[i];
    		if(Array.isArray(item)){
    			func(item);
    		}else{
    			res.push(item);
    		}
    	}
    	return res;
    };
    console.log(func(ary));
    
  4. reduce()方法:

    function flatten(ary){
    	return ary.reduce((pre,cur) =>{
    		return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    	},[])
    }
    console.log(flatten(ary));
    
  5. 使用数组some遍历+扩展运算符:

    while(ary.some(Array.isArray)){
    	ary = [].concat(...ary); 
    }
    console.log(ary);
    

9. js数组乱序

  1. sort + 随机数:

    arr.sort(function(a,b){
    	return Math.random() > 0.5 ? -1 : 1;
    });
    console.log(arr);
    
  2. 洗牌算法:

    function shuffle(arr) {
    	let length = arr.length,
    	r = length,
    	rand = 0;
    	while (r) {
    		rand = Math.floor(Math.random() * r--);
    		[arr[r], arr[rand]] = [arr[rand], arr[r]];
    	}
    	return arr;
    }
    console.log(shuffle(arr));
    

10. js函数柯里化

把接收多个参数的函数 变换成 接收单一参数 的函数,并且 返回接收余下的参数 且 返回结果的 新函数的技术。

使用场景:

  • 参数复用;
  • 提前确认,避免每次都重复判断;
  • 延迟计算/运行;
// 普通say函数
function say(school,age,name){
	console.log(`我的学校是${school},年龄是${age},名字是${name}`);
}
say('xxx',24,'Bob');
say('xxx',24,'Marry');
say('xxx',24,'Lisa');
// 柯里化
function say(school){
	return function(age){
		return function(name){
			console.log(`我的学校是${school},年龄是${age},名字是${name}`)
		}
	}
}
let setSchool = say('xxx');
let setAge = setSchool(24);
setAge('Bob');
setAge('Marry');
setAge('Lisa');

// 参数复用
let setInfo = say('xxx')(24);
setInfo('Bob');
setInfo('Marry');
setInfo('Lisa');
// 封装一个公用 函数柯里化的方法
// 参数fn:被柯里化的函数
function curry(fn){
	// 记录fn的参数个数
	let len = fn.length;
	return function temp(){
		// 收集本次传递的参数
		let args = [...arguments];
		if(args.length >= len){
			return fn(...args);
		}else{
			return function(){
				temp(...args, ...arguments);
			}
		}
	}
}
let r = curry(say);
// r('xxx')(24)('Lisa');

// 参数复用
let r1 = r('xxx')(24);
r1('Marry');

11. js数组的合并

  1. concat 方法:
var a=[1,2,3],b=[4,5,6];
var c=a.concat(b);
console.log(c); // 1,2,3,4,5,6
console.log(a); // 1,2,3  不改变本身
  1. 循环遍历:
var arr1=['a','b'];
var arr2=['c','d','e'];
 for(var i=0;i<arr2.length;i++){
      arr1.push(arr2[i]) 
}
console.log(arr1);//['a','b','c','d','e']
  1. apply:合并数组arr1和数组arr2,使用Array.prototype.push.apply(arr1,arr2) 或arr1.push.apply(arr1,arr2);
var arr1=['a','b'];
var arr2=['c','d','e'];
Array.prototype.push.apply(arr1,arr2);
//或者
arr1.push.apply(arr1,arr2);<br>console.log(arr1) //['a','b','c','d','e']
  1. ES6写法——扩展运算:
// ES6 的写法
var a = [0, 1, 2];
var b = [3, 4, 5];
a.push(...b);
console.log(a)//[0,1,2,3,4,5]

// ES6 的写法
var a = [0, 1, 2];
var b = [3, 4, 5];
var c=[...a, ...b]
console.log(c)//[0,1,2,3,4,5]
  1. ES6写法——使用map():
var arr1 = [1, 2, 3];
var arr2 = ["a","b","c","d","e","f"];
arr1.map(item=>{
	arr2.push(item)
});
console.log(arr2) // [1, 2, 3, "a", "b", "c", "d", "e", "f"]

12. js对象的合并

  1. $.extend():
var obj1= {'a': 1};
var obj2= {'b': 1};
var c = $.extend(obj1, obj2);
console.log(obj1); // {a: 1, b: 1}  obj1已被修改
//或者 <br>var obj3 = $.extend({}, obj1, obj2) <br>console.log(obj3); //{a: 1, b: 1} 不会改变obj1,obj2
  1. 遍历赋值:
var obj1={'a':1};
var obj2={'b':2,'c':3};
for(var key in obj2){
     if(obj2.hasOwnProperty(key)===true){//此处hasOwnProperty是判断自有属性,用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问会避免原型对象扩展带来的干扰
           obj1[key]=obj2[key];
} 
}
console.log(obj1);//{'a':1,'b':2,'c':3};
  1. Obj.assign() :可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。Object.assign(target, …sources)
//a. 复制一个对象<br>var obj = { a: 1 ,b:2};
var copyObj = Object.assign({}, obj);
console.log(copyObj); // { a: 1,b:2 }
//b.合并多个对象
var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1);  // { a: 1, b: 2, c: 3 }, 且目标对象自身也会改变。
  1. 对象的深拷贝和浅拷贝:

    4.1 浅拷贝

var obj1={'a':1};
var obj2={'b':{'b1':22,'b2':33}};

$.extend(obj1, obj2);   //obj1拷贝了obj2的属性

console.log(obj1)  // {'a':1,'b'{'b1':22,'b2':33}}
console.log(obj1.b.b1)  // 22

obj2.b.b1=44;   //obj2重新赋值
console.log(obj1.b.b1)  // 44  obj1.b仅拷贝了对象的指引,所以受原obj2的影响 

4.2 深拷贝

var obj1={'a':1};
var obj2={'b':{'b1':22,'b2':33}};

$.extend(true,obj1, obj2);   //第一个参数设为true表示深复制

console.log(obj1)  // {'a':1,'b'{'b1':22,'b2':33}}
console.log(obj1.b.b1)  // 22

obj2.b.b1=44;   //obj2重新赋值
console.log(obj1.b.b1)  // 22 obj1拷贝了obj2的所有属性以及值,并不受obj2的影响

13. let var const区别

  • let是块状作用域,而var是函数作用域。

  • let: let 不能被重新定义,允许你声明一个作用域被限制在块级中的变量、语句或者表达式,let 绑定不受变量提升的约束,这意味着let声明不会被提升到当前,该变量处于从块开始到初始化处理的"临时死区"。

  • var: 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的, 由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明。

  • const 声明创建一个值的只读引用 (即指针),这里就要介绍下 JS 常用类型: String、Number、Boolean、Array、Object、Null、Undefined。其中基本类型有 Undefined、Null、Boolean、Number、String,保存在栈中;复合类型 有 Array、Object ,保存在堆中; 基本数据当值发生改变时,那么其对应的指针也将发生改变,故造成 const申明基本数据类型时,再将其值改变时,会造成报错, 例如 const a = 3 ; a = 5 时 将会报错;但是如果是复合类型时,如果只改变复合类型的其中某个Value项时, 还是正常使用。

  1. var声明的变量会挂载到window上,而let和const的变量不会;
  2. var声明的变量存在变量提升,let和const不存在变量提升;
  3. let和const声明形成块作用域;
  4. 同一作用域下,let和const不能声明同名变量,而var可以;
  5. let声明的是变量,const声明的是常量,只读,修改值会报错,const保存的是内存地址,可以给数组或对象添加属性或元素,但是不能重复复写。

声明提升:JS在编译阶段,编译器的一部分工作就是找到所有声明,并用合适的作用域将他们关联起来。所有声明(变量和函数)都会被移动到各自作用域的最顶端,这个过程被称为提升。

14. JS强制类型转换函数

  • 函数parseInt:强制转换成整数。例如parseInt(“6.12”)=6 ; parseInt(“12a")=12 ; parseInt(“a12")=NaN ;parseInt(“1a2")=1
  • 函数parseFloat:强制转换成浮点数。parseFloat(“6.12”)=6.12
  • 函数eval:将字符串强制转换为表达式并返回结果。eval(“1+1”)=2 ; eval(“1<2”)=true

15. Parseint()、Math.round()、Math.floor()、Math.ceil()四种取整方法的区别?

  • Parseint():直接舍弃小数部分,只保留整数部分。parseInt(string, radix) 函数可解析一个字符串,并返回一个整数。string:必需,要被解析的字符串;radix:可选。表示要解析的数字是几进制的,解析出的结果是十进制数。参数介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。可用来进制转换。
  • Math.round():表示四舍五入取整。
  • Math.floor():floor地板,表示向下取整。
  • Math.ceil():ceil天花板,表示向上取整。

16. null和undefined的区别?

  • null转为数字类型为0,undefined转为数字类型为NaN;
  • undefined是代表一个值而该值却没有赋值,这时候默认为undefined;
  • null是一个很特殊的对象,最为常见的用法就是作为参数传入;
  • 设置为null的变量或者对象会被内存收集器回收;

null是表示空值,对其做typeof ,得到的是object,是一个特殊的对象 。

undefined表示“缺少值”,就是此处应该有一个值,但是还没有定义。

(1)变量被声明了,但没有赋值时,就等于undefined。

(2)调用函数时,应该提供的参数没有提供,该参数就等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

17. JS深浅拷贝

ECMAScript中的数据类型可分为两种:(判断基本数据类型用typeOf,判断引用类型用instanceOf)

  • 基本类型:undefined, null, Boolean, String, Number, Symbol;
  • 引用类型:Object, Array, Date, Function, RegExp等;

不同类型的存储方式:

  • 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中。
  • 引用类型:引用类型的值是对象,保存在堆内存中,而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。

不同类型的复制方式:

  • 基本类型:从一个变量向另外一个新变量复制基本类型的值,会创建这个值的一个副本,并将该副本复制给新变量。修改其中一个的值不会改变另一个值。
  • 引用类型:从一个变量向另一个新变量复制引用类型的值,其实复制的是指针,最终两个变量都指向同一个对象。修改其中一个的值会影响另一个值。

浅拷贝: 仅仅是复制了引用,彼此之间的操作会互相影响。

深拷贝: 在堆中重新分配内存,不同的地址,相同的值,互不影响。

深拷贝的实现:

  1. 递归实现:

    //递归方法,进行深拷贝
    function deepClone(obj) {
         //传进来的参数如果是数组选择[],如果不是数组选择{}
         let objClone = Array.isArray(obj) ? [] : {};
         //判断传进来的是不是object类型,如果是基本类型就直接返回。
         if(obj && typeof obj === "object") {
               for(key in obj) {  //枚举遍历
                    if(obj.hasOwnProperty(key)) {  //判断obj是否有该名称的属性或对象
                       //判断ojb子元素是否为对象,如果是,递归复制
                       if(obj[key] && typeof obj[key] === "object") {
                             objClone[key] = deepClone(obj[key]);
                       } else {//当递归到子元素不是对象时、简单复制
                          //如果不是,简单复制
                          objClone[key] = obj[key];
                      }
                 }
            }
       }
       return objClone;
    }        
    
  2. json实现:

    var result = JSON.parse(JSON.stringify(people))
    

18. JS闭包的概念?优缺点?

闭包的概念:闭包就是能读取其他函数内部变量的函数。

闭包的定义即:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

优点:

  1. 避免全局变量的污染;
  2. 希望一个变量长期存储在内存中(缓存变量);

缺点:

  1. 内存泄露(消耗);
  2. 常驻内存,增加内存使用量;
// 闭包
function foo(){
    var local = 1
    function bar(){
        local++;
        return local
    }
    return bar
}

var func = foo()
func()

19. js 执行机制、事件循环、异步编程方法

JavaScript 语言的一大特点就是单线程,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript 语言的设计者意识到这个问题,将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous),在所有同步任务执行完之前,任何的异步任务是不会执行的。当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数。当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的 Event Loop(事件循环)。我们不禁要问了,那怎么知道主线程执行栈为空啊?js 引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数。

说完 JS 主线程的执行机制,下面说说经常被问到的 JS 异步中 宏任务(macrotasks)微任务(microtasks) 执行顺序。JS 异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入 Event Queue,然后再执行微任务,将微任务放入 Event Queue,但是,这两个 Queue 不是一个 Queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的 Queue 拿宏任务的回调函数。

  • 宏任务:当前调用栈中执行的任务称为宏任务。(主代码块,定时器等等),整体代码 script,setTimeout,setInterval。
  • 微任务: 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务为微任务。(可以理解为回调事件,promise.then,process.nextTick等等)。
  • 宏任务中的事件放在callback queue中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。

异步编程方法:

  1. 回调函数:给耗时长的函数添加回调函数,这个回调函数是原本这个任务执行完成之后要执行的函数,来达到避免后续函数执行被前面的函数阻塞。被作为参数传递到另一个函数(主函数)的那个函数就叫做 回调函数

  2. promise对象:ES6新特性Promise,可以将异步操作以同步操作的流程表达出来,避免层层嵌套的回调函数。new Promise()构造函数接受一个函数作为参数,这个函数又接受两个参数分别为resolve和reject方法。如果异步操作完成,就调用resolve方法将Promise对象实例的状态变为“成功”(从pending到resolved);如果异步操作失败,就用reject方法将Promise对象实例的状态变成失败(从pending到rejected)。

    .then方法,分别给resolve方法和reject方法指定回调函数。

  3. 事件监听:给耗时长的函数绑定一事件,事件触发后就执行后来的函数。

20. this的指向问题,普通函数和箭头函数的this,怎么改变普通函数里面的this指向?

普通函数调用时的this指向问题:

1、纯函数调用时,this指向window;
2、事件调用时,谁调用指向谁;
3、定时器调用时,this指向window;
4.、构造函数调用时,this指向构造函数生成的这个新的对象;
5、apply 调用时,this指向它的第一个参数。

this指向如何改变?

JS中改变this指向有三个封装好的方法:
1、call(参数1(写谁是谁),实参)
2、apply(参数1(写谁是谁),[N个实参])
3、bind()返回值为一个修改完this指向的函数,需要主动调用。
例:

 let  f=fn.bind(this.a.b...)
 f();

箭头函数和普通函数的区别?

1、箭头函数作为匿名函数,不能作为构造函数,不能使用new运算符;
2、箭头函数不绑定auguments,取而代之的使用rest参数代替;
3、箭头函数不绑定this,但会捕获其上下文的this值,作为自己的this值;
4、箭头函数通过call()或者apply()方法调用一个函数时,只传入一个参数,对this没有影响;
5、箭头函数当方法使用的时候,没有定义this绑定;
6、箭头函数没有原型属性;
7、箭头函数不能当做Generator函数,不能使用yiel关键字。

总之,箭头函数的this指向永远指向其上下文的this,任何方法都改变不了,而普通函数的this指向调用它的那个对象。

普通函数中的this:

1.this总是代表它的直接调用者(js的this是执行上下文), 例如 obj.func ,那么func中的this就是obj;

2.在默认情况(非严格模式下,未使用 ‘use strict’),没找到直接调用者,则this指的是 window (常见的window的属性和方法有: alert, location,document,parseInt,setTimeout,setInterval等)(约定俗成);

3.在严格模式下,没有直接调用者的函数中的this是 undefined;

4.使用call,apply,bind(ES5新增)绑定的,this指的是绑定的对象;

箭头函数中的this:

箭头函数没有自己的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象, 定义它的时候,可能环境是window; 箭头函数可以方便地让我们在 setTimeout ,setInterval中方便的使用this。即使是call,apply,bind等方法也不能改变箭头函数this的指向。

要整明白这些, 我们需要首先了解一下作用域链:当在函数中使用一个变量的时候,首先在本函数内部查找该变量,如果找不到则找其父级函数,最后直到window,全局变量默认挂载在window对象下。

this的四种绑定规则:

  • 默认绑定:全局范围内,没有直接调用者,this指的是Window对象;若是严格模式,则this为undefined。

  • 隐式绑定:当调用一个对象的方法时,就会发生隐式绑定,this指向调用该方法的对象。

  • 硬绑定:用call和apply方法,就不会把原对象的"小红"暴露出来,只会打印"小白"和"小黄"。想绑定哪个对象,就绑定哪个对象。

  • 构造函数绑定:创建一个构造函数然后进行实例化,this就会和实例化后的新对象进行捆绑。

21. call和apply的区别?

  • call的语法:函数名.call(obj,参数1,参数2,参数3……);

  • apply的语法:函数名.apply(obj,[参数1,参数2,参数3……]);

相同点:

1.call()和apply()都可以用来间接调用函数,都可以显式调用所需的this。即,任何函数可以作为任何对象的方法来调用。

2.两个方法都可以指定调用实参。

区别:

call()和apply()的基本区别:在于将参数传递给函数。

call(): 需要使用逗号分隔列出所有参数。

apply():要求以数组的形式传入参数,需要注意的是即使只有一个参数,也必须写在数组里面。

22. apply、call、bind区别?以及如何用apply实现bind函数?

  1. call 方法第一个参数是this的指向,后面传入的是一个参数列表。当第一个参数为null、undefined的时候,默认指向window。如:getColor.call(obj, ‘yellow’, ‘blue’, ‘red’);
  2. apply 方法接受两个参数,第一个参数是this的指向,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window。如:getColor.apply(obj, [‘yellow’, ‘blue’, ‘red’]);
  3. bind 方法和 call 方法很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。低版本浏览器没有该方法,需要自己手动实现。

在JavaScript中,call和apply都是为了改变某个函数运行时的上下文而存在的,换句话说就是为了改变函数体内部的this指向。bind是返回对应函数,便于稍后使用; apply、call则是立即调用。

<script>
	let person1 = {
        name:"张三",
        age:12,
        say(...args){   // 剩余参数语法允许我们将一个不定数量的参数表示为一个数组
            console.log(`姓名:${this.name},年龄:${this.age},参数:${args}`)
            let args = arguments
            console.log(args)
            args = Array.prototype.slice.call(args,0)
            args.push(8)   // push是真数组才有的方法
            console.log(args)
        }    
    }
	// person2想借用person1中的say方法
    let person2 = {    
            name:"李四",
            age:23,  
    }   
    // 1.call 第一个参数:借用方法的对象(这里是person2借用say方法),后面还可以传递多个参数
    person1.say.call(person2,1234,2,4)

	// 2.apply 第一个参数:借用方法的对象(这里是person2借用say方法),允许传递的第二个参数必须是数组
	person1.say.apply(person2,[1234,2,4])

	// 3.bind 第一个参数:借用方法的对象(这里是person2借用say方法),后面还可以传递多个参数,因为bind          方法返回值为函数,所以执行要加括号
	person1.say.bind(person2,1234,2,4)()

	//会遇到的面试题:
		// 1) 如何将伪数组转换成真数组(见上)
			// 伪数组:就是具备length属性,但不能使用真数组的方法
			// 怎么产生的伪数组:querySelectAll,getElementsByClassName,getElementsByTagName,arugments
		// 2) 怎么无侵入无序数组求最大值
			let arr = [12,3,56,8,666]
            let res1 = Math.max.apply(null,arr)
            let res2 = Math.min.call(null,...arr)
            console.log(res1)
			console.log(res2)
    
</script>

手动用apply实现bind函数:

Function.prototype.myBind = function (obj = window, ...args){
	let self = this;
	return function(...params){
		return self.apply(obj, args.concat(params))
	}
}

23. DOM 事件有哪些阶段(事件流)?谈谈对事件代理/事件委托的理解

img

图为事件流的全过程,从图中我们可以看出:

  • 一个完整的事件流是从window开始,最后回到window的一个过程;
  • 事件流被分为3个阶段:1-5捕获阶段,5-6:目标阶段,6-10:冒泡阶段。

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。事件不直接绑定到某元素上,而是绑定到该元素的父元素上,进行触发事件操作时(例如’click’),再通过条件判断,执行事件触发后的语句(例如’alert(e.target.innerHTML)’)。

事件委托得以实现的前提是事件具有冒泡机制,就像涟漪一样,子元素的事件会继续蔓延传递到父元素上,那么就可以通过监听父元素,再利用target判断事件的具体触发源头,进而监听父元素内任意子元素的事件。

Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用event.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题):

window.onload = function(){
  var oUl = document.getElementById("ul1");
  oUl.onclick = function(ev){
  	 // 兼容性处理:
    var event = e || window.event;
    var target = event.target || event.srcElement;
    
    if(target.nodeName.toLowerCase() == 'li'){
         alert(123);
         alert(target.innerHTML);
    }
  }
}

事件委托的优点,一是可以将批量元素监听的任务通过一个监听实现,优化页面性能;二是可以实现对动态生成子元素的监听(新添加的子元素是带有事件效果的),某些场景下非常有用。好处:(1)使代码更简洁;(2)节省内存开销。

24. 一个DOM元素绑定多个事件时,先执行冒泡还是捕获?

无论是冒泡事件还是捕获事件,元素都会先执行捕获阶段。从上往下,如果有捕获事件,则执行;一直下到目标元素后,从目标元素开始向上执行冒泡元素,即第三个参数为false的绑定事件的元素。(在向上执行过程中,已经执行过的捕获事件不再执行,只执行冒泡事件)。

25. DOM事件event对象中target和currentTarget的区别?

target是事件触发的真实元素;currentTarget是事件绑定的元素。

this和currentTarget的关系:在事件处理程序内部,对象this始终等于currentTarget的值。

currentTarget和target,有时候是同一个元素,有时候不是同一个元素 (因为事件冒泡)

  • 当事件是子元素触发时,currentTarget为绑定事件的元素,target为子元素。
  • 当事件是元素自身触发时,currentTarget和target为同一个元素。

26. JS中DOM事件中一些常见的浏览器兼容问题

  1. 事件监听/事件绑定:

    • node.addEventListener(事件类型,事件处理函数,false) IE9以下不兼容。

    • node.attachEvent(“on”+事件类型,事件处理函数) IE11以下兼容,11不兼容。

    所以需要封装兼容写法:

    function addEvent(node,etype,fn){
        return node.addEventListener node.addEventListener(etype,fn,false):node.attachEvent("on"+etype,fn)
    }
    `node代表节点对象;etype表示事件类型;fn为事件处理函数`
    
  2. 事件冒泡:常见缺点:就是具有穿透性,易多级触发具有相同事件的父元素事件,同时覆盖外部相同事件。
    一般处理事件冒泡主要有两种方法:

    • event.stopPropagation()方法, 适用于标准浏览器,IE9以下不支持。
    • event.cancelBubble = true; IE全系列支持。

    兼容写法:

    function stopBubble(event){
    	return event.stopPropagation?event.stopPropagation():event.cancelBubble = true;
    }
    
  3. 事件触发对象:目标获取:主要依靠事件下的属性获取;

    • event.target; 此方法标准浏览器兼容,IE9以下不兼容。
    • event.srcElement; 这是IE方法,全系列支持。

    兼容写法:利用短路运算;

    var  target = event.target || event.srcElement;
    

    可以配合事件冒泡,进行事件委托,所谓事件委托就是需要对子元素进行事件操作,而不是在子元素上直接添加事件,改由在父元素上添加事件,同时获取事件对象后后,使用event.target指向当前操作的子元素。

  4. 默认事件的阻止:

    • event.preventDefault() 标准浏览器 IE9以上支持,以下不支持
    • event.returnValue = false; IE9以下支持,以上不支持

    可以看出IE9之前使用 event.returnValue = false; 来阻止默认事件,从IE9开始改为使用标准方法event.preventDefault() 阻止默认行为。
    需要兼容IE9以下的需要考虑兼容:

    function stopDefault(event){
    	return event.preventDefault?event.preventDefault():event.returnValue = false;
    }
    

27. JavaScript绑定事件的方法

  1. 在DOM元素中直接绑定:属性赋值 ,这个在该元素的properties属性中可以查到, 也可以在事件监听中看到。

    function show(){
      console.log('show');
    }
    function print(){
      console.log('print');
    }
    
    //触发的方法只有show方法:
    <button onclick="show()" id="btn1" onclick="print()">html标签事件绑定</button>
    //一个事件,触发两个方法:
    <button onclick="show();print()" id="btn1">html标签事件绑定</button>
    
  2. 在JavaScript代码中绑定:属性赋值,这个在该元素的properties属性中可以查到,也可以在事件监听中看到。

    <button id="btn2">js事件绑定</button>
    
    <script type="text/javascript">  
        document.getElementById("btn2").onclick = show;
        document.getElementById("btn2").onclick = print;
    </script>  
    //注:给一个事件绑定多个操作只会执行最后一个
    //只能触发print方法,如果需要同时触发两个方法,只能使用事件监听
    
  3. 绑定事件监听函数:用 addEventListener() 或 attachEvent() 来绑定事件监听函数,只可以在该元素的事件监听中看到。

    <button id="btn3">事件监听</button>      
    
    //show和print两个方法都可以触发
    document.getElementById("btn3").addEventListener("click",show);
    document.getElementById("btn3").addEventListener("click",print);
    
    //移除事件监听
    document.getElementById("btn3").removeEventListener("click");
    
    //注意,这里的事件名称没有"on",如鼠标单击事件 click ,鼠标双击事件 doubleclick ,鼠标移入事件 mouseover,鼠标移出事件 mouseout 等。
    

28. jQuery中绑定事件on和bind的区别?

.bind(events [,eventData], handler)

.on(events [,selector] [,data], handler)

从上可以看出,.on方法比.bind方法多一个参数’selector’,.on的selector参数是筛选出调用.on方法的dom元素的指定子元素,如:$(‘ul’).on(‘click’, ‘li’, function(){console.log(‘click’);})就是筛选出ul下的li给其绑定click事件。selector参数的好处是可以进行事件委托。

29. js原型链 (https://www.cnblogs.com/jzhey/p/12582506.html)

每个构造函数都有一个prototype属性,我们称之为显式原型对象,每个实例对象都有一个__proto__属性,我们称之为隐式原型对象,他们都是指向同一个原型对象。

function Animal(name,age) {
    this.name = name
    this.age = age
}
let pig = new Animal('peiqi',3)
console.log(pig)
  1. 任何的对象都是new Object()出来的。

  2. Object()也是一个构造函数,所以他在栈上存了一个Object的变量,对应的是堆上的函数对象,这个对象在堆上的内存地址是0x444,并且里面还装了一个prototype属性(构造函数身上必有一个prototype属性),他在堆内存中也开了一块地址,去存他的prototype,内存地址为0x555。

  3. 原型对象(不管是显式原型对象还是隐式原型对象)都是普通对象,他不能加()运行,他相当于是通过new Object()产生的,所以所有的普通对象都有一个__proto__属性,并且

    普通对象.__proto__ === Object.prototype
    
  4. Animal的显式原型对象也是普通对象,所以他的显式原型对象中有一个__proto__属性,它指向Object的显式原型对象。

  5. Object.prototype._proto_ = null 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oD8tROPU-1600183118869)(C:%5CUsers%5CJoe%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200804105157846.png)]

构造函数的prototype属性指向构造函数的原型,构造函数原型的contructor属性指向构造函数,构造函数生成的实例的\__proto__属性指向构造函数的原型。

JavaScript中的对象,都有一个内置属性[[protoType]],指向这个对象的原型对象。当查找一个属性或者方法时,如果在当前对象找不到定义,会继续在当前对象的原型对象中查找;如果原型对象中依然没有找到,会继续在原型对象的原型中查找(原型也是对象,也有它自己的原型);如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。

30. 原型对象和构造函数

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会自带一个prototype属性,这个属性指向函数的原型对象。当函数经过new调用时,这个函数就成为了构造函数,返回了一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。

// Foo构造函数,foo为Foo的一个实例对象
foo.__proto__ === Foo.prototype

另外,尽量用 Foo.prototype 来访问原型,而不要用 _proto_ 来访问原型,如果一定要通过实例访问原型,可以用ES6的 Object.getPrototypeOf(foo)。

console.log(foo.__proto__ === Foo.prototype);  // true
console.log(Object.getPrototypeOf(foo) === Foo.prototype);  // true
console.log(Object.getPrototypeOf(foo) === foo.__proto__);  // true

31. js new一个对象的过程

function Mother(lastName){
	this.lastName = lastName
}
var son = new Mother("Da");
  1. 创建一个新对象:son;

  2. 新对象会被执行[[prototype]]连接;

    son.__proto__ = Mother.prototype;
    
  3. 新对象和函数调用的this会绑定起来;

    Mother.call(son,"Da");
    
  4. 执行构造函数中的代码;

    son.lastName;
    
  5. 如果函数没有返回值,那么就会自动返回这个新对象。

    return this;
    

32. js继承的几种方式及优缺点?

  1. 原型链继承:将子对象的prototype指向父对象的一个实例。引用类型的属性被所有实例共享,且创建子类实例时,无法向父类构造函数传参。

    // 父类型
    function Supper(){
        this.supProp = 'Supper property';
        this.a = [1,2,3,4];
    }
    Supper.prototype.showSupperProp = function(){
    	console.log(this.supProp)
    };
    
    // 子类型
    function Sub(){
    	this.subProp = 'Sub property';
    }
    // 子类型的原型为父类型的一个实例对象(原型链继承的关键一步)
    Sub.prototype = new Supper();
    // 让子类型的原型的constructor指向子类型
    Sub.prototype.constructor = Sub;
    Sub.prototype.showSubProp = function(){
    	console.log(this.subProp)
    };
    
    var sub = new Sub();
    sub.showSubProp();
    sub.showSupperProp();
    
    // 引用类型的属性被所有实例共享
    var sub1 = new Sub();
    var sub2 = new Sub();
    sub1.a.push(5);
    console.log(sub1.a);
    console.log(sub2.a);
    

在这里插入图片描述

  1. 借用构造函数:使用call或apply方法,将父对象的构造函数绑定在子对象上。解决了引用值共享的问题,但只能继承父类的实例属性和方法,不能继承原型属性/方法,没办法拿到原型上的方法。

    // 借用构造函数继承(假的继承)
    function Person(name,age){
        this.name = name;
        this.age = age;
    }
    function Student(name,age,price){
    // 在子类型构造函数中通过call()调用父类型构造函数
    	Person.call(this,name,age);  // 相当于 this.Person(name.age)
    	this.price = price;
    }
    
    var s = new Student('Tom',20,14000);
    console.log(s.name,s.age,s.price)
    
  2. 组合继承(伪经典继承):组合继承是比较常用的一种继承方法,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。注意要修正子类型的原型的constructor指向子类型。缺点:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)。

    // 原型链+借用构造函数的组合继承
    // 1.利用原型链实现对父类型对象的方法继承
    // 2.利用call()借用父类型构造函数初始化相同属性
    function Person(name,age){
        this.name = name;
        this.age = age;
    }
    Person.prototype.setName = function(name){
    	this.name = name;
    };
    function Student(name,age,price){
        // 在子类型构造函数中通过call()调用父类型构造函数
        Person.call(this,name,age);  // 为了得到属性,借用构造函数
        this.price = price;
    }
    Student.prototype = new Person(); // 原型链继承,为了能拿到父类型的方法
    Student.prototype.constructor = Student; // 修正constructor属性
    Student.prototype.setPrice = function(price){
    	this.price = price;
    };
    
    var s = new Student('Tom',18,12000);
    s.setName('Bob');
    s.setPrice(15000);
    console.log(s.name,s.age,s.price)
    
  3. 寄生组合继承(经典继承):Sub.prototype = Object.create(Super.prototype) 解决了组合继承父类调用两次的问题。

    function Super(){
    	this.a = [1,2,3,4];
    }
    Super.prototype.say = function(){
    	console.log(222);
    };
    function Sub(){
    	Super.call(this);
    }
    // 兼容性写法:
    if(!Object.create){
    	Object.create = function(proto){
    		function F(){}
    		F.prototype = proto;
    		return new F();
    	};
    }
    // ES5写法:
    // 指定Sub.prototype的原型为Super.prototype
    Sub.prototype = Object.create(Super.prototype); 
    var sub1 = new Sub();
    var sub2 = new Sub();
    // sub1.a = '333';
    sub1.a.push(5);
    console.log(sub1.a);
    console.log(sub2.a);
    // sub1.say();
    // sub2.say();
    
  4. extends继承:

    // ES6支持的class语法糖
    class Super{
    
    }
    class Sub extends Super{
    
    }
    
  5. 拷贝继承

  6. 圣杯模式继承

33. javascript 的垃圾回收机制

如果没有垃圾回收机制,javascript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。由于字符串,数组,对象这些引用类型,给他们分配内存的时候是根据他们的大小进行动态分配的,动态分配的被占用的这些内存,最终都要释放掉以供再次利用。现在各个浏览器基本常采用标志清除法,和引用计数法来回收垃圾;

  1. 标记清除法:
当变量进入执行环境时,标记这个变量为“进入环境”;
当变量离开环境时,标记为“离开环境”;
垃圾回收器并不是每时每刻都在工作,每隔一段时间工作一次;
垃圾回收器运行时,给每个变量都加上标记,然后去掉环境中的变量,以及被环境中变量引用的变量的标记。
然后,这时候,剩下的所有的被标记的变量,都是要被清除回收的变量。
最后,垃圾收集器销毁这些变量的值,收回占用的内存空间。
  1. 引用计数法
跟踪每个值被引用的次数。
当声明了一个变量,把一个引用类型的值复制给这个变量时,这个值的引用次数就是1;
如果这个变量又重新引用了另一个值,那么原先的值引用次数就减1;
当一个值的(被)引用次数变成0时,就说明不再也没有办法再引用这个值了。因此就可以将他的内存释放。
垃圾收集器在下次运行时,就会回收释放掉这种引用次数为0的值的内存。
注意会存在循环引用的问题!

34. js调bug?

  1. debugger:在JavaScript代码中加入一句debugger;来手工造成一个断点效果。需要带有条件的断点吗?你只需要用 if 语句包围它:

    if (somethingHappens) {
      debugger;
    }
    
  2. 设置在DOM node发生变化时触发断点:谷歌浏览器的开发工具里有一个超级好用的功能,专门可以对付这种情况,叫做*“Break on…”*,你在DOM节点上右键,就能看到这个菜单项。断点的触发条件可以设置成这个节点被删除、节点的属性有任何变化,或它的某个子节点有变化发生。

    https://img.jbzj.com/file_images/article/201403/2014030709595010.jpg

35.ajax请求时get和post的区别?

get:从服务器上获取数据,传送数据量小,安全性低,请求会被缓存,缓存是针对URL进行缓存的,get请求参数直接加在URL地址后面,一种参数组合就会产生一种URL的缓存,重复的请求结果是相同的;、

post:向服务器发送数据;传送数据量大,请求不会被缓存,参数封装在二进制的数据体中,服务器也不会记录参数,相对安全,所以涉及用户隐私的数据都要用post传送;

36. ajax请求时,如何解析json数据?

json是一种轻量级交互格式,本质上都是字符串,常用于前后端的数据交互,本质上就是字符串。

  • 前端解析后端数据:前端在解析后端发来的数据,使用JSON.parse()方法把字符串转为json对象。
  • 前端向后端发送数据数据:前端在向后端发送数据,使用JSON.stringify()方法把json对象转为字符串。

37. 跨域、同源策略以及解决跨域

在前端开发编码过程中,常见的html标签例如:a, form, img, script, link, iframe 以及ajax操作都可以指向一个资源地址或者说可以发起对一个资源的请求,那么这里所说的请求就存在同域请求还是跨域请求。跨域是浏览器行为,不是服务器行为。

出于浏览器的同源策略限制。同源策略SOP(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器很容易受到XSS、CSFR等攻击。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。跨域原因产生:在当前域名请求网站中,默认不允许通过ajax请求发送其他域名。非同源限制:1.无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB;2.无法接触非同源网页的 DOM;3.无法向非同源地址发送 AJAX 请求。

跨域解决方法:

  1. JSONP 是服务器与客户端跨源通信的常用方法。jsonp原理就是内部会创建一个<script>标签,把想要请求的url加到src里,通过他的src属性进行跨域,然后把<script>标签动态的填加到页面上,请求发过去拿到数据之后再动态的把这个标签删除。最大特点就是简单适用,兼容性好(兼容低版本IE),缺点是只支持get请求,不支持post请求。核心思想:网页通过添加一个<script>元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

    • 原生js实现jsonp:
    <button onclick="f()">submit</button>
    <script>
        function addScriptTag(src){
         var script = document.createElement('script');
             script.setAttribute("type","text/javascript");
             script.src = src;
             document.body.appendChild(script);
             document.body.removeChild(script);
        }
        function SayHi(arg){
            alert("Hello "+arg)
        }
    
        function f(){
             addScriptTag("http://127.0.0.1:8002/get_byjsonp/?callbacks=SayHi")
        }
    </script>
    
    ----------------------views.py
    def get_byjsonp(req):
        func=req.GET.get("callbacks")
        return HttpResponse("%s('joe')"%func)
    
    • jQuery实现jsonp:

      <script type="text/javascript" src="/static/jquery-2.2.3.js"></script>
      <script type="text/javascript">
         $.ajax({
              url:"http://127.0.0.1:8002/get_byjsonp",
              dataType:"jsonp",            //必须有,告诉server,这次访问要的是一个jsonp的结果。
              jsonp: 'callbacks',          //jQuery帮助随机生成的:callbacks="wner"
              success:function(data){
                  alert(data)
              }
         });
      </script>
      
      #-------------------------------------http://127.0.0.1:8002/get_byjsonp
      def get_byjsonp(req):
          callbacks=req.GET.get('callbacks')
          print(callbacks)                 #wner  
      	return HttpResponse("%s('joe')"%callbacks)
      
  2. CORS 是跨域资源共享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request),对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

    1、普通跨域请求:只需服务器端设置Access-Control-Allow-Origin = ‘指定域名’;

    2、带cookie跨域请求:前后端都需要进行设置。服务器端设置 Access-Control-Allow-Credentials: true,在AJAX请求中打开 withCredentials 属性:

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    
  3. 使用nginx反向代理:Nginx 反向代理模块 proxy_pass,proxy_pass 后面跟着一个 URL,用来将请求反向代理到 URL 参数指定的服务器上。例如 proxy_pass https://api.shanbay.com,则将匹配的请求反向代理到 https://api.shanbay.com。通过在配置文件中增加proxy_pass 你的服务器ip,例如这里的扇贝服务器地址,就可以完成反向代理。

    server {
        listen       80;
        server_name  localhost;
        ## 用户访问 localhost,则反向代理到https://api.shanbay.com
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass https://api.shanbay.com;
        }
    }
    

    代理服务器,客户机在发送请求时,不会直接发送给目的主机,而是先发送给代理服务器,代理服务接受客户机请求之后,再向主机发出,并接收目的主机返回的数据,存放在代理服务器的硬盘中,再发送给客户机。反向代理(Reverse Proxy)方式是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。通常的代理服务器,只用于代理内部网络对Internet的连接请求,客户机必须指定代理服务器,并将本来要直接发送到Web服务器上的http请求发送到代理服务器中。当一个代理服务器能够代理外部网络上的主机,访问内部网络时,这种代理服务的方式称为反向代理服务。

    • 正向代理,架设在客户机与目标主机之间,只用于代理内部网络对Internet的连接请求,客户机必须指定代理服务器,并将本来要直接发送到Web服务器上的http请求发送到代理服务器中。隐藏了真实的客户端。
    • 反向代理服务器架设在服务器端,通过缓冲经常被请求的页面来缓解服务器的工作量,将客户机请求转发给内部网络上的目标服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器与目标主机一起对外表现为一个服务器。隐藏了真实的服务器。

38. scrollWidth、clientWidth、offsetWidth、innerWidth 区别

  • scrollWidth :是对象的实际内容的宽,不包含边线宽度,会随对象中内容的多少改变(内容多了可能会改变对象的实际宽度)。
  • clientWidth :对象内容的可视区的宽度,不包滚动条等边线,会随对象显示大小的变化而改变。
  • offsetWidth :对象整体的实际宽度,包滚动条等边线,会随对象显示大小的变化而改变。
  • innerWidth:window.innerWidth=浏览器窗口的内部宽度;window.innerHeight=浏览器窗口的内部高度。

39. promise和async/await

为了解决Callback hell回调地狱,promise和async/await诞生。

  • promise的作用是对异步回调代码包装一下,把原来的一个回调函数拆成2个回调函数,这样的好处是可读性更好。语法如下:

    注意:Promise内部的resolve和reject方法只能调用一次,调用了这个就不能再调用那个了;如果调用,则无效。

    // 用promise对含有回调方法的代码进行包装
    let promise = new Promise((resolve, reject)=>{
        // resolve方法是当异步操作成功时候调用
        // reject方法是当异步操作出现异常时调用
        fs.readFile("test/a.txt", (err, data)=>{
            if(err){
                // 说明有异常
                reject(err);
            }else{
                // 说明异步操作是成功的
                resolve(data);
            }
        })
    });
    // 直接使用promise,问题是仍然需要传回调,不建议直接使用
    promise.then((data)=>{
        console.log("异步操作成功" + data.toString());
    }).catch((err)=>{
        console.log("出异常" + err);
    });
    
  • async/await的作用是直接将Promise异步代码变为同步的写法,注意,代码仍然是异步的。

    语法要求:

    • await只能用在async修饰的方法中,但是有async不要求一定有await。
    • await后面只能跟async方法和promise。

    假设拥有了一个promise对象,现在使用async/await可以这样写:

    // 使用async/await语法来调用promise对象
    async function asyncDemo(){
        // 使用try/catch捕获promise内部的异常
        try{
            let data = await promise;
            console.log(data.toString());
        }catch(e){
            console.log("promise内部出异常了:" +e);
        }
    }
    asyncDemo();
    

40. async/await 和 Promise的区别?

  1. async/await是写异步代码的新方式,以前的方法有回调函数和Promise。
  2. async/await是基于Promise实现的,它不能用于普通的回调函数。
  3. async/await与Promise一样,是非阻塞的。
  4. async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

41. forEach 和 map的区别

  • foreEach() 方法: 针对每一个元素执行提供的函数。
  • map() 方法: 创建一个新的数组,其中每一个元素由调用数组中的每一个元素执行提供的函数得来。

区别:forEach() 方法不会返回执行结果,而是undefined。也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。

42. 防抖和节流

防抖:事件响应函数(doSomeThing)在一段时间后(300ms)才执行,如果在这段时间再次调用,则重新计算执行时间;当预定的时间内没有再次调用该函数,则执行事件响应函数doSomeThing。

应用场景:

  1. scroll事件滚动触发;
  2. 搜索框输入验证;
  3. 表单验证;
  4. 按钮提交事件;
  5. 浏览器窗口缩放,resize事件;
// 第一版
// 缺点:函数的 this 指向了 window,应该指向 container
function debounce1(func, wait) {
    let timer;
    return function () {
        clearTimeout(timer);
        timer = setTimeout(func, wait);
    }
}

// 第二版(解决this指向问题)
// 缺点:函数的事件对象 event 变成了 undefined
function debounce2(func, wait) {
    let timer;
    return function () {
        clearTimeout(timer);
        // console.log(this);  // 这里的 this 是对的
        timer = setTimeout(() => {
            func.call(this)    //绑定上面的 this
        }, wait);
    }
}

// 第三版(解决 event 事件对象问题)
function debounce3(func, wait) {
    let timer;
    return function () {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, arguments)   // 把参数传进去
        }, wait);
    }
}

// 第四版(immediate参数为true或false的情况)
// immediate为true就是第一次会先直接执行一次,刚开始timer为undefined,callNow为true就立即执行;接着在wait时间内一直操作,timer就是有值的,callNow为false就不执行;等停止操作wait时间后,timer被赋为null了,这下再操作又会立即执行。
function debounce4(func, wait, immediate) {
    let timer;
    return function () {
        if (timer) clearTimeout(timer);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, wait)
            // 立即执行
            if (callNow) func.apply(this, arguments);
        } else {
            timer = setTimeout(() => {
                func.apply(this, arguments);
            }, wait);
        }
    }
}

container.onmousemove = debounce4(doSomeThing,2000,true);

节流:如果持续触发事件,每隔一段时间,只执行一次事件。在规定时间内,保证执行一次该函数。

应用场景:

  1. DOM元素的拖拽功能实现;
  2. 射击游戏;
  3. 计算鼠标移动的距离;
  4. 监听scroll滚动事件;
// 使用时间戳实现节流:第一次触发,最后一次不会触发
// 顾头不顾尾
function throttle1(func,wait){
    let context,args;
    // 之前的时间戳
    let old = 0;
    return function(){
        context = this;
        args = arguments;
        // 获取当前的时间戳
        let now = new Date().valueOf();
        if(now-old > wait){
            // 立即执行
            func.apply(context,args);
            old = now;
        }
    }
}

// 使用定时器:第一次不会触发,最后一次会触发
// 不顾头顾尾
function throttle2(func,wait){
    let context,args,timeout;
    return function(){
        context = this;
        args = arguments;
        if(!timeout){
            timeout = setTimeout(() =>{
                timeout = null;
                func.apply(context,args);
            }, wait);
        }
    }
}

// 时间戳+定时器:第一次会执行,最后一次还会执行
// 顾头又顾尾
function throttle3(func,wait){
	let timer;
    let old = 0;
    return function(){
        let context = this;
        let args = arguments;
        let now = new Date().valueOf();
        if(now-old > wait){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            func.apply(context,args);
            old = now;
        }
        if(!timer){
            timer = setTimeout(() =>{
                old = new Date().valueOf();
                timer = null;
                func.apply(context,args);
            }, wait);
        }
    }
}
// 上面代码稍微写好看点
function throttle3(func,wait){
    let context,args,timeout;
    let old = 0;   // 时间戳
    let later = function(){
        old = new Date().valueOf();
        timeout = null;
        func.apply(context,args);
    };
    return function(){
        context = this;
        args = arguments;
        let now = new Date().valueOf();
        if(now-old > wait){
            if(timeout){
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context,args);
            old = now;
        }else if(!timeout){
            timeout = setTimeout(later,wait);
        }
    }
}

// 节流完整版
function throttle4(func,wait,options){
    let timer;
    let old = 0;
    if(!options) options = {};
    return function(){
        let context = this;
        let args = arguments;
        let now = new Date().valueOf();
        if(options.leading === false && !old){
            old = now;
        }
        if(now-old > wait){
            if(timer){
                clearTimeout(timer);
                timer = null;
            }
            func.apply(context,args);
            old = now;
        }
        if(!timer && options.trailing !== false){
            timer = setTimeout(() =>{
                timer = null;
                old = new Date().valueOf();
                func.apply(context,args);
            }, wait);
        }
    }
}
// 上面代码稍微写好看点
function throttle4(func,wait,options){
    let context,args,timeout;
    let old = 0;   // 时间戳
    if(!options){options = {}}
    let later = function(){
        old = new Date().valueOf();
        timeout = null;
        func.apply(context,args);
    };
    return function(){
        context = this;
        args = arguments;
        let now = new Date().valueOf();
        if(options.leading === false && !old){
            old = now;
        }
        if(now-old > wait){
            // 第一次会直接执行
            if(timeout){
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context,args);
            old = now;
        }else if(!timeout && options.trailing !== false){
            // 最后一次会执行
            timeout = setTimeout(later,wait);
        }
    }
}

container.onmousemove = throttle4(doSomeThing,2000,{
    leading: true,
    trailing: false
});

43. js中 == 和 === 的区别?

双等号 ==: 可转换类型再比较

(1)如果两个值类型相同,再进行三个等号(===)的比较;

(2)如果两个值类型不同,也有可能相等,需根据以下规则进行类型转换在比较:
    1)如果一个是null,一个是undefined,那么相等;
    2)如果一个是字符串,一个是数值,把字符串转换成数值之后再进行比较;

三等号 ===:不能转换类型

(1)如果类型不同,就一定不相等;

(2)如果两个都是数值,并且是同一个值,那么相等;如果其中至少一个是NaN,那么不相等。(判断一个值是否是NaN,只能使用isNaN( ) 来判断);

(3)如果两个都是字符串,每个位置的字符都一样,那么相等,否则不相等;

(4)如果两个值都是true,或是false,那么相等;

(5)如果两个值都引用同一个对象或是函数,那么相等,否则不相等;

(6)如果两个值都是null,或是undefined,那么相等;

44. 弹出输入框

<body>
    <input type="button" onclick="disp_prompt()" value="test" />
    <script>
        function disp_prompt(){
	        var name = prompt("请输入您的名字",""); // 弹出input框
	        alert("您的名字是:"+name);
        }
    </script>
</body>

45. js函数调用时传递的隐式参数

在函数调用的时候,浏览器每次都会传递进两个隐式参数:

  1. 函数的上下文对象this;

  2. 封装实参的对象arguments;

function fun(){
	console.log(arguments);   // 打印出的arguments见下
}
fun('Tom',[1,2,3],{name:'Jerry'}); 
Arguments(3) ["Tom", Array(3), {…}, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    0: "Tom"
    1: (3) [1, 2, 3]
    2: {name: "Jerry"}
    callee: ƒ fun()
    length: 3
    Symbol(Symbol.iterator): ƒ values()
    __proto__: Object

================================================================
以上为个人学习笔记总结,参考了许多网上大神的文章,供大家学习参考交流,如有版权问题请联系我,侵删。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值