js面试准备(自留,持续更新)

本文详细总结了JavaScript面试中的常见问题,包括数据类型(原始类型、对象类型)、ES6新特性(let、const、箭头函数、Promise)、任务调度(宏任务与微任务)以及js单线程原因、setTimeout和setInterval区别。此外,还探讨了函数、原型、继承、异步处理和作用域等方面的知识,是准备JavaScript面试的宝贵资料。
摘要由CSDN通过智能技术生成

本文集成多篇博客以及权威书籍加上自己的理解汇总,以下是借鉴的博客网址:
https://blog.csdn.net/qq_33277654/article/details/122924692
https://blog.csdn.net/cc18868876837/article/details/81211729
https://blog.csdn.net/hualvm/article/details/84395850
https://blog.csdn.net/nicexibeidage/article/details/78144138
后盾人网站:https://doc.houdunren.com
《js红宝书》

1. js数据类型有哪些?

6+1 USONB
6种简单数据类型(原始类型):undefined、null、boolean、number、string、symbol。
*基本数据类型是存储在栈中,栈内存是自动分配内存的,赋值时传值。
1种复杂数据类型:Object对象(细分有Object、Array、Function、Date、RegExp)。
*复杂数据类型时存储在堆中,堆内存是动态分配的,不会自动释放,赋值时传址。

1)typeof操作符
需要注意的是typeof是一个操作符而不是函数,因此不需要参数。
调用typeof null时返回的是“object”,这是因为特殊值null被认为是一个对空对象的引用
严格来讲函数在js里被认为是对象,而不是数据类型,但函数有自己的特性,因此需要通过typeof来区分函数和其他对象

2)Undefined类型
该类型只有一个值就是特殊值undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值(const在声明后必须赋值否则报错)。
增加undefined这个特殊值的目的就是为了正式明确空对象指针null和未初始化变量的区别
当输出未初始化变量和未声明变量时,会出现下述情况:输出未初始化变量a的值为undefined,要输出未声明变量b则会报错。

let a;
// let b;
console.log(a);
console.log(b);

在这里插入图片描述
对于未声明的变量,只能执行一个有用的操作,就是对它调用typeof。未初始化的变量a的类型是undefined,而对未声明的变量b调用结果还是undefined。

let a;
// let b;
console.log(typeof a);
console.log(typeof b);

在这里插入图片描述

3)Null类型
该类型也同样只有一个特殊值null,null表示一个空对象指针,因此typeof null会返回object。
undefined值是由null值派生而来的,表面上两者相等,用等于操作符(==)比较时始终返回true,但要注意这个操作符会为了比较而转换它的操作数。

console.log(null == undefined);

在这里插入图片描述
但两者用途完全不一样。永远不用显式地将变量值设为undefined,但null不是这样,如果变量要保存对象而当时有没有哪个对象可以保存,就可以用null来填充。

4)Boolean类型
有两个字面量值:true和false。这两个布尔值不同于数值,true不等于1,false不等于0。
类型转换:Boolean()

数据类型转换为true的值转换为false的值
String非空字符串“”
Number非零数值0、NaN
Object任意对象null

5)Number类型
(1)浮点值
定义浮点值必须包含小数点,且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但是推荐加上。比如:

let a = 1.1;
let b = 0.1;
let c = .1; //不推荐

存储浮点值使用的内存空间是存储整数值的两倍,所以js会想方设法把浮点值转为整数。小数点后面没有数字或者小数点后的数字是0,那么就会被转换为整数。比如:

let a = 10.; //小数点后面没有数,被当为整数
let b = 1.0; //小数点后面是0,被当为整数

对于过大或者过小的数值,浮点值可以用科学计数法来表示。js中科学计数法格式要求是一个数值后跟一个大写或小写的字母e,再加上一个要乘的10的多少次幂。比如:

let num = 3.15e7; //3.15乘10的7次幂=31500000
console.log(num);

浮点值精确的最高可达17位小数,但远不如整数精确。例如0.1+0.2得到的不是0.3,由于这种微小的舍入错误,导致很难测试特定的浮点值。之所以存在这种舍入错误是因为使用了IEEE 754数值,其他使用相同格式的语言也有这个问题。

(2)值的范围
js可以表示的最小数值保存在Number.MIN_VALUE中,最大数值保存在Number.MAX_VALUE中。如果计算得到的数值超出了js可表示范围,那么这个数值会被自动转换为一个特殊的Infinity值。正Infinity或者负Infinity值将不能再进一步用于任何计算,要确定一个值是否为有限大,可以使用isFinite()函数。如下:

let max = Number.MAX_VALUE;
let min = Number.MIN_VALUE;
console.log(max); //1.7976931348623157e+308
console.log(min); //5e-324

let result = max + max;
console.log(isFinite(result));//false

(3)NaN
Not a Number,用于表示本来要返回数值的操作失败了。
在js里,0、+0、-0相除会返回NaN

console.log(0 / 0); //NaN
console.log(0 / -0); //NaN
console.log(0 / +0); //NaN

如果分子是非0值,分母是有符号0或无符号0,则会返回Infinity或-Infinity。

console.log(5 / -0); //-Infinity
console.log(5 / +0); //Infinity

NaN有几个特殊属性:
1.任何涉及NaN的操作始终返回NaN,在连续多步计算时可能是个问题。
2.NaN不等于包括NaN在内的任何值

console.log(NaN / 10); //NaN
console.log(NaN == NaN); //false

为此js提供了isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。该函数会尝试把传入的值转换为数值

console.log(isNaN(NaN)); //true
console.log(isNaN(10)); //false,10是数值
console.log(isNaN("10")); //false,可以转换为数值10
console.log(isNaN("blue")); //true,不可以转换为数值
console.log(isNaN(true)); //false,可以转换为数值1

(4)类型转换为数值
Number() pareseInt() parseFloat()
Number()可以用于任何数据类型,后两个函数主要用于将字符串转换为数值。

Number():

console.log(Number(true)); //1
console.log(Number(3)); //3
console.log(Number(null)); //0
console.log(Number(undefined)); //NaN
console.log(Number("011")); //11
console.log(Number("Hello World")); //NaN

parseInt():
更专注于字符串是否包含数值模式,字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立刻返回NaN,依次检测至字符串末尾或非数值字符。(意味着空字符串会返回NaN)

console.log(parseInt("")); //NaN
console.log(parseInt("1234blue")); //1234
console.log(parseInt("22.5")); //22
console.log(parseInt("70")); //70

parseFloat():
跟parseInt()类似。parseFloat()始终忽略字符串开头的0,如果字符串表示整数(没有小数点或者小数点后面只有一个0),则返回整数。

console.log(parseFloat("1234blue")); //1234
console.log(parseFloat("22.5")); //22.5
console.log(parseFloat("22.5.49")); //22.5
console.log(parseFloat("0987.6")); //987.6
console.log(parseFloat("3.15e7")); //31500000

6)String类型
(1)转换为字符串
toString() String() +""

toString():
几乎所有值都有的toString方法,唯一的用途就是返回当前值的字符串等价物。字符串本身也有toString()方法,但只是简单地返回自身的一个副本。null和undefined值没有toString()方法。

String():
如果不确定值是不是null或undefined,可以使用String()转型函数。

(2)模板字面量
es6新增特性,模板字面量保留换行字符,可以跨行定义字符串。

let a = 'first line\nsecond line';
let b = `first line
second line`;
console.log(a);
//first line
//second line
console.log(b);
//first line
//second line

使用${}实现字符串插值

(3)模板字面量标签函数
模板字面量通过定义标签函数来自定义插值行为。
对于有n个插值的模板字面量,传递给标签函数表达式参数个数始终是n,而传给标签函数第一个参数所包含的字符串个数始终是n+1.(通过${}划分)

let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
    console.log(strings);
    for (const exp of expressions) {
        console.log(exp);
    }
    return 'foobar';
}
let untaggedResult = `${a}+${b}=${a + b}`;
let taggedResult = simpleTag`${a}+${b}=${a + b}`;
//['', '+', '=', '']
//6
//9
//15
console.log(untaggedResult);//6+9=15
console.log(taggedResult);//foobar

7)Symbol类型
8)Object类型
js中对象通过new创建。
每个Object都有如下属性和方法:
constructor:用于创建当前对象的函数。
hasOwnProperty(propertyName):用于判断当前实例(不是原型)上是否存在给定的属性。
isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
propertyIsEnumerable(propertyName):用于判断给定属性是否可以使用for-in语句枚举。
toString():返回对象的字符串表示。
valueOf():返回对象对应的字符串、数值或布尔值表示。

2.es6新特性都有什么?

1)let、const声明变量
let声明变量特点:

  1. 不允许重复声明
  2. 块级作用域
  3. 不存在变量提升
  4. 不影响作用域链
    什么是作用域链:就是代码块内有代码块,跟常规编程语言一样,上级代码块中的局部变量下级可用

const声明常量特点:

  1. 声明必须赋初始值
  2. 标识符一般为大写
  3. 不允许重复声明
  4. 值不允许修改
  5. 块级作用域

2)变量解构赋值

  1. 数组解构赋值
const F4 = ["大哥","二哥","三哥","四哥"]; 
let [a,b,c,d] = F4; // 这就相当于我们声明4个变量a,b,c,d,其值分别对应"大哥","二哥","三哥","四哥" 
console.log(a + b + c + d); // 大哥二哥三哥四哥 
  1. 对象解构赋值
const F3 = { name : "大哥",
            age : 22,
            sex : "男",
            xiaopin : function(){ 
                console.log("我会演小品!"); 
            }
}
let {name,age,sex,xiaopin} = F3; // 注意解构对象这里用的是{} 
console.log(name + age + sex + xiaopin); // 大哥22男 
xiaopin(); // 此方法可以正常调用

3)模板字面量

  1. 字符串保留换行符
  2. 可以使用${xxx}形式引用变量

4)对象的简化写法
允许在大括号里直接写入变量和函数作为对象的属性和方法。
4. 完整写法
5. 简化写法(当对象属性名和变量名相同时)
6. 声明方法的简化

5)箭头函数
特性:

  1. 如果形参只有一个,则小括号可以省略
  2. 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果
  3. 箭头函数的this是静态的,不能通过apply、call、bind更改指向,箭头函数本身没有this,只能继承,始终指向函数声明时所在作用域下的this的值,this指向父级函数的this指向
  4. 箭头函数不能作为构造函数实例化
  5. 不能使用arguments

(1)定时器
传统函数的问题:定时器setTimeout()和setInterval()的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的,调用的代码运行在与所在函数完全分离的执行环境上。

let ad = document.getElementById('ad');
ad.addEventListener('click', function () {
    setTimeout(function () {
        console.log(this); //window
        this.style.background = 'pink';
    }, 2000);
});

使用箭头函数可以解决:因为箭头函数的this指向的是父级函数指向的this,也就是div元素。

let ad = document.getElementById('ad');
ad.addEventListener('click', function () {
    setTimeout(() => {
        console.log(this); //div元素
        this.style.background = 'pink';
    }, 2000);
});

(2)事件处理函数
事件处理函数使用箭头函数的时候,this指向的是父级函数指向的this,也就是window

let ad = document.getElementById('ad');
ad.addEventListener('click', () => {
    console.log(this); //window
});

(3)对象方法函数
func1:是普通函数,因此this指向当前对象obj
func2:是箭头函数,this指向父级函数的this指向,因此是window
func4:是箭头函数,this指向父级函数func3的this指向,是obj

const obj = {
    name: '张三',
    age: 18,
    sex: '男',
    fun1: function () {
        console.log(this)
    },
    fun2: () => {
        console.log(this)
    },
    fun3: function () {
        const fun4 = () => {
            console.log(this);
        }
        fun4();
    }
};

obj.fun1(); //obj
obj.fun2(); //window
obj.fun3(); //obj

6)函数参数的默认值
es6允许给函数参数赋值初始值

  1. 形参初始值:具有默认值的参数,一般位置要靠后(潜规则)
  2. 与解构赋值结合

7)rest参数(…args)
ES6 引入 rest 参数,用于获取函数的实参,用来代替 arguments
ES5获取实参使用arguments参数,而且获得的是Object类型。
注意:

  1. rest参数获得的是一个数组
  2. rest参数必须放在最后
// ES6的rest参数...args,rest参数必须放在最后面 
function data(...args){ 
    console.log(args); // fliter some every map
} 
data("大哥","二哥","三哥","四哥");

8)扩展运算符(…)
…扩展运算符能将 “数组” 转换为 “逗号分隔的参数序列”,好比rest参数的逆运算,对数组进行解包。
两者的区别:

  1. rest参数是定义在声明函数的形参中,是将参数转化为数组
  2. 扩展运算符是定义在调用函数的实参中,是将数组转化为参数
//声明一个数组 ... 
const tfboys = ['易烊千玺', '王源', '王俊凯']; 
// 声明一个函数 
function chunwan() {console.log(arguments);} 
chunwan(...tfboys); // chunwan('易烊千玺','王源','王俊凯')

具体应用:
(1)数组的合并

const kuaizi = ['王太利','肖央']; const fenghuang = ['曾毅','玲花']; 
// es5传统的合并方式 
// const zuixuanxiaopingguo = kuaizi.concat(fenghuang); 
const zuixuanxiaopingguo = [...kuaizi, ...fenghuang]; 
console.log(zuixuanxiaopingguo);

(2)数组的克隆
如果有引用类型,则是浅拷贝。

const sanzhihua = ['E','G','M']; 
const sanyecao = [...sanzhihua];// ['E','G','M'] 
console.log(sanyecao);

(3)将伪数组转为真正的数组

const divs = document.querySelectorAll('div'); 
const divArr = [...divs]; 
console.log(divArr); // arguments

9)Symbol类型
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。
类似于身份证,参数代表姓名,姓名可以重复,但是身份证号不可以。
特点:

  1. Symbol的值是唯一的,用来解决命名冲突的问题
// 没有参数的情况
let name1 = Symbol();
let name2 = Symbol();
name1 === name2  // false
name1 === name2 // false

// 有参数的情况
let name1 = Symbol('flag');
let name2 = Symbol('flag');
name1 === name2  // false
name1 === name2 // false
  1. Symbol值不能与其他数据进行运算
//不能与其他数据进行运算
//let result = s + 100;
//let result = s > 100; 
//let result = s + s;
//报错
  1. Symbol定义的对象属性不能使用for…in循环遍历,但是可以使用Reflect.ownKeys来获取对象的所以键名

(1)Symbol的方法
Symbol.for()
用于将描述相同的Symbol变量指向同一个Symbol值,方便通过描述区分不同的Symbol。

Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol

Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol

var sym = Symbol.for("mario");
sym.toString(); 
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串

Symbol()和Symbol.for()的不同点:

  1. Symbol()创建的symbol不放入全局注册表中,每次都是新建,即使描述值相同也会新建。
  2. Symbol.for()创建的symbol会放入一个全局symbol注册表中。每次创建一个symbol会先检查给定的key是否已经在注册表中,如果在则直接返回上次存储的那个,否则会再新建一个。

(2)用Symbol向对象中添加方法
要向对象里添加方法,但是怕已经存在同名方法,因此使用Symbol

let game = {
    name: '俄罗斯方块',
    up: function () { },
    down: function () { }
};
//要向game对象里添加方法,但是怕已经存在同名方法,因此使用Symbol
//法一:
let method = {
    up: Symbol(),
    down: Symbol()
};
game[method.up] = function () {
    console("new up");
};
game[method.down] = function () {
    console("new down");
};
console.log(game);
//{name: '俄罗斯方块', up: ƒ, down: ƒ, Symbol(): ƒ, Symbol(): ƒ}
//  down: ƒ ()
//  name: "俄罗斯方块"
//  up: ƒ ()
//  Symbol(): ƒ ()
//  Symbol(): ƒ ()

//法二:
let newgame = {
    name: "狼人杀",
    [Symbol("up")]: function () { },
    [Symbol("down")]: function () { }
}
console.log(newgame);
//{name: '狼人杀', Symbol(up): ƒ, Symbol(down): ƒ}
//  name: "狼人杀"
//  Symbol(down): ƒ ()
//  Symbol(up): ƒ ()

(3)Symbol的内置值
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。可以称这些方法为魔术方法,因为它们会在特定的场景下自动执行。
Symbol内置值的使用,都是作为某个对象类型的属性去使用,用来控制对象的表现。

内置Symbol的值调用时机
Symbol.hasInstance当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法
Symbol.isConcatSpreadable对象的 Symbol.isConcatSpreadable 属性等于的是一个布尔值,表示该对象用于 Array.prototype.concat()时,是否可以展开。
Symbol.species创建衍生对象时,会使用该属性
Symbol.match当执行 str.match(myObject) 时,如果该属性存在,会调用它,返回该方法的返回值。
Symbol.replace当该对象被 str.replace(myObject)方法调用时,会返回该方法的返回值。
Symbol.search当该对象被 str. search (myObject)方法调用时,会返回该方法的返回值。
Symbol.split当该对象被 str. split (myObject)方法调用时,会返回该方法的返回值。
Symbol.iterator对象进行 for…of 循环时,会调用 Symbol.iterator 方法,返回该对象的默认遍历器
Symbol.toPrimitive该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
Symbol. toStringTag在该对象上面调用 toString 方法时,返回该方法的返回值。
Symbol. unscopables该对象指定了使用 with 关键字时,哪些属性会被 with环境排除。

Symbol.hasInstance

class Person {
    static [Symbol.hasInstance](param) {
        console.log(param);
        console.log("我被用来检测类型了");
        return false;
    }
}
let o = { name: 'Alison' };
console.log(o instanceof Person);
//Object name: "Alison"
//我被用来检测类型了
//false

Symbol.isConcatSpreadable

const arr = [1, 2, 3];
const arr2 = [4, 5, 6];
// 合并数组:false数组不可展开,true可展开 
arr2[Symbol.isConcatSpreadable] = false;
console.log(arr.concat(arr2));
//[1, 2, 3, Array(3)]

10)迭代器iterator接口
**迭代器可以用来自定义遍历对象。**它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费。
原生具备 iterator 接口的数据(可用 for of 遍历):

  • Array;
  • Arguments;
  • Set;
  • Map;
  • String;
  • TypedArray;
  • NodeList;

工作原理:

  1. 创建一个指针对象,指向当前数据结构的起始位置;
  2. 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员;
  3. 接下来不断调用 next 方法,指针一直往后移动,直到指向最后一个成员;
  4. 每调用 next 方法返回一个包含 value 和 done 属性的对象;
// 声明一个数组 
const xiyou = ['唐僧', '孙悟空', '猪八戒', '沙僧'];
// 使用 for...of 遍历数组 
for (let v of xiyou) {
    console.log(v);
}
let iterator = xiyou[Symbol.iterator]();

//调用对象的next方法
console.log(iterator.next());//{value: '唐僧', done: false}
console.log(iterator.next());//{value: '孙悟空', done: false}
console.log(iterator.next());//{value: '猪八戒', done: false}
console.log(iterator.next());//{value: '沙僧', done: false}
console.log(iterator.next());//{value: undefined, done: true}

//重新初始化对象,指针也会重新回到最前面
let iterator1 = xiyou[Symbol.iterator]();
console.log(iterator1.next());//{value: '唐僧', done: false}

迭代器自定义遍历对象:

// 声明一个对象 
const banji = {
    name: "终极一班",
    stus: ['xiaoming', 'xiaoning', 'xiaotian', 'knight'],
    [Symbol.iterator]() {
        // 索引变量 
        let index = 0;
        // 保存this 
        let _this = this;
        return {
            next: function () {
                if (index < _this.stus.length) {
                    const result = { value: _this.stus[index], done: false };
                    // 下标自增
                    index++;
                    // 返回结果 
                    return result;
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
}

// 遍历这个对象 
for (let v of banji) {
    console.log(v);
}
//xiaoming xiaoning xiaotian knight

11)Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,Promise 将异步操作队列化,按照期望的顺序执行,返回符合预期的结果,可以通过链式调用多个 Promise 达到我们的目的。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果(通常是一个异步操作的结果)。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise特点:

  1. Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

(1)基本用法

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});
promise.then(function() {
  console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve函数和reject函数。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

(2)then
一个promise 需要提供一个then方法访问promise 结果,then 用于定义当 promise 状态发生改变时的处理,即promise处理异步操作,then 用于结果。
promise 就像 kfc 中的厨房,then 就是我们用户,如果餐做好了即 fulfilled ,做不了拒绝即rejected 状态。那么 then 就要对不同状态处理。

  1. then 方法必须返回 promise,用户返回或系统自动返回
  2. 第一个函数在resolved 状态时执行,即执行resolve时执行then第一个函数处理成功状态
  3. 第二个函数在rejected状态时执行,即执行reject 时执行第二个函数处理失败状态,该函数是可选的
  4. 两个函数都接收 promise 传出的值做为参数
  5. 如果 then 返回 promise ,下一个then 会在当前promise 状态改变后执行
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("操作成功");
  }, 2000);
});
const p2 = new Promise((resolve, reject) => {
  resolve(p1);
}).then(
  msg => {
    console.log(msg);
  },
  error => {
    console.log(error);
  }
);

如果只关心成功则不需要传递 then 的第二个参数,如果只关心失败时状态,then 的第一个参数传递 null。

const promise = new Promise((resolve, reject) => {
  reject("is error");
});
promise.then(null, error => {
  console.log(`失败:${error}`);
});

promise 传向then的传递值,如果then没有可处理函数,会一直向后传递。

let p1 = new Promise((resolve, reject) => {
	reject("rejected");
})
.then()
.then(
  null,
  f => console.log(f)
);

Promise解决过程也可以处理其他类型,如果该类型有then方法,程序会把他封装成promise进行处理。

let p1 = new Promise((resolve, reject) => {
    resolve("fulfilled");
}).then(
    value => {
        return class {
            static then(resolve, reject) {
                resolve("这是一个静态方法");
            }
        };
    },
    reason => { }
).then(value => {
    console.log(value);
})

链式调用:每一个then都是对前一个返回的Promise的处理。每次的 then 都是一个全新的 promise,默认 then 返回的 promise 状态是 fulfilled。如果 then 返回promise 时,后面的then 就是对返回的 promise 的处理,需要等待该 promise 变更状态后执行。

(3)catch
catch用于失败状态的处理函数,等同于 then(null,reject){}

  1. 建议使用catch处理错误
  2. 将catch放在最后面用于统一处理前面发生的错误

错误是冒泡的操作的,下面没有任何一个then 定义第二个函数,将一直冒泡到 catch 处理错误。

new Promise((resolve, reject) => {
  reject(new Error("请求失败"));
})
.then(msg => {})
.then(msg => {})
.catch(error => {
  console.log(error);
});

catch 也可以捕获对 then 抛出的错误处理。

new Promise((resolve, reject) => {
  resolve();
})
.then(msg => {
  throw new Error("这是then 抛出的错误");
})
.catch(() => {
  console.log("33");
});

catch 也可以捕获其他错误,下面在 then 中使用了未定义的变量,将会把错误抛出到 catch。

new Promise((resolve, reject) => {
  resolve("success");
})
.then(msg => {
  console.log(a);
})
.catch(reason => {
  console.log(reason);
});

但像下面的在异步中 throw 将不会触发 catch,而使用系统错误处理。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("fail");
  }, 2000);
}).catch(msg => {
  console.log(msg + "后盾人");
});

(4)finally
无论状态是resolve 或 reject 都会执行此动作,finally 与状态无关。

const promise = new Promise((resolve, reject) => {
  reject("hdcms");
})
.then(msg => {
  console.log("resolve");
})
.catch(msg => {
  console.log("reject");
})
.finally(() => {
  console.log("resolve/reject状态都会执行");
});

(5)resolve
有时需要将现有对象转为 Promise 对象,使用 promise.resolve() 方法可以快速的返回一个Promise对象。(等价于new Promise(resolve => resolve()))

promise.resolve() 方法的参数分四种情况:

  1. 参数是一个Promise实例 :将不做任何修改、原封不动地返回这个实例。
  2. 参数是一个thenable对象 :会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。
  3. 参数不是具有then方法的对象或者不是对象 :返回一个新的 Promise 对象,状态为resolved。
  4. 不带任何参数:直接返回一个resolved状态的 Promise 对象。所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。
Promise.resolve("后盾人").then(value => {
  console.log(value); //后盾人
});

(6)reject
和 Promise.resolve 类似,reject 生成一个失败的promise。

new Promise(resolve => {
  resolve("后盾人");
})
.then(v => {
  if (v != "houdunren.com") return Promise.reject(new Error("fail"));
})
.catch(error => {
  console.log(error);
});

(7)all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。使用Promise.all 方法可以同时执行多个并行异步操作,比如页面加载时同时获取课程列表与推荐课程。

  1. 适用于一次发送多个异步操作
  2. 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例
  3. const p = Promise.all([p1, p2, p3]); 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const hdcms = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第一个Promise");
  }, 1000);
});
const houdunren = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("第二个异步");
  }, 1000);
});
const hd = Promise.all([hdcms, houdunren])
  .then(results => {
    console.log(results);
  })
  .catch(msg => {
    console.log(msg);
  });
 // ['第一个Promise', '第二个异步']

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

(8)allSettled
有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。Promise.all()方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。为了解决这个问题,引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)
Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const p1 = new Promise((resolve, reject) => {
    resolve("resolved");
});
const p2 = new Promise((resolve, reject) => {
    reject("rejected");
});
Promise.allSettled([p1, p2])
    .then(msg => {
    console.log(msg);
})
//(2) [{…}, {…}]
//  0: {status: 'fulfilled', value: 'resolved'}
//  1: {status: 'rejected', reason: 'rejected'}
//  length: 2

(9)race
使用Promise.race() 处理容错异步,和race单词一样哪个Promise快用哪个,哪个先返回用哪个。

  1. 以最快的promise为准
  2. 如果最快返加的状态为rejected 那整个promise为rejected执行catch
  3. 如果参数不是promise,内部将自动转为promise
const hdcms = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("第一个Promise");
    }, 2000);
});
const houdunren = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("第二个异步");
    }, 1000);
});
Promise.race([hdcms, houdunren])
    .then(results => {
    console.log(results);
})
    .catch(msg => {
    console.log(msg);
});
//第二个异步

12)Set&WeakSet
ES6 提供了新的数据结构 Set(集合)。它类似于数组,但成员的值都是唯一的,集合实现了 iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历。
特点:

  1. 值是唯一的
  2. 严格类型检测,"1"不等于1
  3. 遍历顺序是添加的顺序

Set的属性和方法:

  1. size 返回集合的元素个数;
  2. add 增加一个新元素,返回当前集合;
  3. delete 删除元素,返回 boolean 值;
  4. has 检测集合中是否包含某个元素,返回 boolean 值;
  5. clear 清空集合,返回 undefined;

基本用法:

let set = new Set([2, 3, 4]);
set.add(1);
set.add(1);
set.add('1');
console.log(set);

(1)数组去重
使用展开运算符

let arr = [1, 1, 2, 3, 4, 4, 5, 6];
//使用展开运算符
let result = [...new Set(arr)];
console.log(result);//[1, 2, 3, 4, 5, 6]

(2)交集

let set1 = new Set([1, 3, 5, 5, 7, 9]);
let set2 = new Set([1, 1, 2, 4, 5, 6, 7]);
let newSet = new Set([...set1].filter(item => set2.has(item)));
console.log(newSet);//Set(3) {1, 5, 7}

(3)并集

let set1 = new Set([1, 3, 5, 5, 7, 9]);
let set2 = new Set([1, 1, 2, 4, 5, 6, 7]);
let union = new Set([...set1, ...set2]);
console.log(union); //Set{1,3,5,7,9,2,4,6}

(4)差集
交集的取反。
需要注意是谁和谁求差集,比如集合1和集合2求差集,就是1里面有的,2里面没的。

let set1 = new Set([1, 3, 5, 5, 7, 9]);
let set2 = new Set([1, 1, 2, 4, 5, 6, 7]);
let newSet = new Set([...set1].filter(item => !set2.has(item)));
console.log(newSet);//Set(2) {3, 9}

WeakSet特性:

  1. WeakSet结构同样不会存储重复的值,它的成员必须只能是对象类型的值。
  2. 垃圾回收不考虑WeakSet,即被WeakSet引用时引用计数器不加一,所以对象不被引用时不管WeakSet是否在使用都将删除
  3. 因为WeakSet 是弱引用,由于其他地方操作成员可能会不存在,所以不可以进行forEach( )遍历等操作
  4. 也是因为弱引用,WeakSet 结构没有keys( ),values( ),entries( )等方法和size属性
  5. 因为是弱引用所以当外部引用删除时,希望自动删除数据时使用 WeakMap
    在这里插入图片描述

13)Map&WeakMap
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 也实现了iterator 接口,所以可以使用『扩展运算符』和 『for…of…』进行遍历。具有极快的查找速度。

Map 的属性和方法:

  1. size 返回 Map 的元素个数
  2. set 增加一个新元素,返回当前 Map,支持链式操作
  3. get 返回键名对象的键值
  4. has 检测 Map 中是否包含某个元素,返回 boolean 值
  5. clear 清空集合,返回 undefined
    操作类似于set
// 创建一个空 map 
let m = new Map();
// 创建一个非空 map 
let m2 = new Map([['name', 'Alison'], ['slogon', '狗狗']]);
// 1. size 返回 Map 的元素个数; 
console.log(m2.size);
// 2. set 增加一个新元素,返回当前 Map; 
m.set("皇帝", "大哥");
m.set("丞相", "二哥");
console.log(m);
// 3. get 返回键名对象的键值; 
console.log(m.get("皇帝"));
// 4. has 检测 Map 中是否包含某个元素,返回 boolean 值; 
console.log(m.has("皇帝"));
// 5. clear 清空集合,返回 undefined; 
m.clear();
console.log(m);

WeakMap特性:

  1. 键名必须是对象
  2. WeaMap对键名是弱引用的,键值是正常引用
  3. 垃圾回收不考虑WeaMap的键名,不会改变引用计数器,键在其他地方不被引用时即删除
  4. 因为WeakMap 是弱引用,由于其他地方操作成员可能会不存在,所以不可以进行forEach( )遍历等操作
  5. 也是因为弱引用,WeaMap 结构没有keys( ),values( ),entries( )等方法和 size 属性
  6. 当键的外部引用删除时,希望自动删除数据时使用 WeakMap

14)class类
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

知识点:

  1. class 声明类;
  2. constructor 定义构造函数初始化;
  3. extends 继承父类;
  4. super 调用父级构造方法;
  5. static 定义静态方法和属性;
  6. 父类方法可以重写;

(1)声明类
ES5:

function Phone(brand, price) {
    this.brand = brand;
    this.price = price;
}
// 添加方法
Phone.prototype.call = function () {
    console.log("我可以打电话");
}
// 实例化对象
let HuaWei = new Phone('华为', 5999);
HuaWei.call();//我可以打电话
console.log(HuaWei);//Phone {brand: '华为', price: 5999}

ES6:

class Phone {
    // 构造函数,名字是固定的
    constructor(brand, price) {
        this.brand = brand;
        this.price = price;
    }
    // 党法必须使用该方式写,不能使用call:function(){}
    call() {
        console.log("我可以打电话");
    }
}
let HuaWei = new Phone('华为', 5999);
HuaWei.call();//我可以打电话
console.log(HuaWei);//Phone {brand: '华为', price: 5999}

(2)类静态成员
类也是一个对象,他也有他自己的静态属性,即不会与实例对象公用的属性。

ES5:

function Phone() { }
// 类对象Phone的静态成员
Phone.nickname = "手机";
Phone.change = function () {
    console.log("改变世界");
}
let nokia = new Phone();
console.log(nokia.name);//undefined
nokia.change();//Uncaught TypeError: nokia.change is not a function
console.log(Phone.nickname);//手机
Phone.change();//改变世界

// 实例对象的属性
Phone.prototype.color = "黑色";
console.log(nokia.color);//黑色

ES6:

class Phone {
    static nickname = "手机";
	static change() {
    	console.log("改变世界");
	}
}
let nokia = new Phone();
console.log(nokia.nickname);//undefined
nokia.change();//Uncaught TypeError: nokia.change is not a function

(3)类的继承
ES5:构造函数实现继承

// 父类:手机
function Phone(brand, price) {
    this.brand = brand;
    this.price = price;
}
Phone.prototype.call = function () {
    console.log("打电话");
}
// 子类:智能手机
function SmartPhone(brand, price, color, size) {
    Phone.call(this, brand, price);
    this.color = color;
    this.size = size;
}
// 设置子类构造函数的原型
SmartPhone.prototype = new Phone;

// 声明子类的方法
SmartPhone.prototype.photo = function () {
    console.log("拍照");
}
SmartPhone.prototype.play = function () {
    console.log("玩游戏");
}

const HuaWei = new SmartPhone("华为", 5999, "银色", "5.5inch");
HuaWei.call();//打电话
HuaWei.photo();//拍照
HuaWei.play();//玩游戏

ES6:类继承

class Phone {
    constructor(brand, price) {
        this.brand = brand;
        this.price = price;
    }
    call() {
        console.log("我可以打电话");
    }
}
class SmartPhone extends Phone {
    // 构造函数
    constructor(brand, price, color, size) {
        super(brand, price); //调用父类构造函数
        this.color = color;
        this.size = size;
    }
    photo() {
        console.log("我可以拍照");
    }
    game() {
        console.log("我可以玩游戏");
    }
}
const xiaomi = new SmartPhone("小米", 1999, "黑色", "5.15inch");
console.log(xiaomi);//SmartPhone {brand: '小米', price: 1999, color: '黑色', size: '5.15inch'}
xiaomi.call();//我可以打电话
xiaomi.photo();//我可以拍照
xiaomi.game();//我可以玩游戏

(4)子类对父类方法重写

class Phone {
    constructor(brand, price) {
        this.brand = brand;
        this.price = price;
    }
    call() {
        console.log("我可以打电话");
    }
}
class SmartPhone extends Phone {
    // 构造函数
    constructor(brand, price, color, size) {
        super(brand, price); //调用父类构造函数
        this.color = color;
        this.size = size;
    }
    photo() {
        console.log("我可以拍照");
    }
    game() {
        console.log("我可以玩游戏");
    }
    call() {
        console.log("子类的打电话");
    }
}
const xiaomi = new SmartPhone("小米", 1999, "黑色", "5.15inch");
console.log(xiaomi);//SmartPhone {brand: '小米', price: 1999, color: '黑色', size: '5.15inch'}
xiaomi.call();//子类的打电话
xiaomi.photo();//我可以拍照
xiaomi.game();//我可以玩游戏

(5)getter和setter

class Phone {
    get price() {
        console.log("价格属性被读取");
        return 5999;
    }
    set price(value) {
        console.log("价格属性被修改");
    }
}
let p = new Phone();
console.log(p.price);//价格属性被读取 //5999
p.price = 6000;//价格属性被修改
3.js的任务调度流程?什么是宏任务微任务?

JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop的方案应用而生。

JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环
在这里插入图片描述
js中有两个异步任务队列:宏任务队列微任务队列

  1. 主线程中的同步任务执行完后,才执行异步任务队列中的任务
  2. 有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
  3. 执行优先级:同步任务>微任务队列>宏任务队列

注意点:

  1. 实例化Promise时执行的代码是同步任务
  2. 宏任务:setTimout,setInterval,Dom事件,AJAX。其中的计时是计时器模块从程序运行就开始执行计时,到时间再将任务依次放入宏任务队列。
  3. 微任务:Promise.then(),async/await

1)原理分析

setTimeout(() => {
    console.log("定时器");
    new Promise(resolve => {
        console.log("settimeout Promise");
        resolve();
    }).then(() => {
        console.log("settimeout then");
    });
}, 0);
new Promise(resolve => {//同步
    console.log("Promise");
    resolve();
}).then(() => {//微任务
    console.log("then");
});
console.log("后盾人");

//Promise 后盾人 then 定时器 settimeout Promise settimeout then
  1. 0秒后将setTimeout中的任务加入宏任务队列
  2. 执行实例化Promise中的同步代码,输出"Promise"
  3. 改变状态后,将then()方法中的人物加入微任务队列
  4. 执行同步代码"后盾人"
  5. 同步代码执行完毕后,去微任务队列询问,执行输出"then"
  6. 微任务队列执行完毕后,询问宏任务队列,执行代码
  7. 执行其中同步任务"定时器",以及实例化Promise的同步代码"settimeout Promise"
  8. 改变状态后将then()方法中的任务加入微任务队列
  9. 同步任务执行完毕后,询问微任务队列,并执行输出"settimeout then"

2)脚本加载
引擎在执行同步任务时不会进行DOM渲染,建议将script 放在 BODY 结束标签前,这样会先渲染DOM后再执行任务,页面就不会白页。

<body>
    <h1>houdunren.com</h1>
    <script src="hd.js"></script>
</body>

3)定时器
定时器计时从程序运行开始,到时放入宏任务队列中,等待同步任务执行后才能执行。HTML标准规定最小时间不能低于4毫秒,有些异步操作如DOM操作最低是16毫秒,总之把时间设置大些对性能更好。

4.js为什么是单线程的?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

5.setTimeout和setInterval区别?计时器用哪个?

都是window下的方法

1)setTimeout()
在指定的毫秒数后调用函数或计算表达式,仅执行一次,一般用于延迟执行某方法或功能。
setTimeOut在执行其所要调用的方法时除了计算参数时间(时间间隔)还要等待方法执行时间。

setTimeout(function[, delay, arg1, arg2, ...]);

function sayHi(phrase, who) {
    alert(phrase + ', ' + who);
}
setTimeout(sayHi, 1000, "Hi", "Alison");//参数是函数名
setTimeout('sayHi("Hi","Alison")', 1000);//参数是函数名

销毁方法:clearTimeout(id); 创建定时器时会返回一个id

2)setInterval()
按照指定的周期(以毫秒计)来调用函数或计算表达式,方法会不停的调用函数,直到clearInterval()被调用或者窗口被关闭,用于刷新表单。
setInterval在执行其所要调用的方法时严格按照参数时间(时间间隔)执行,不会等待方法执行时间,如果执行方法的时间超过了参数时间(时间间隔),则setInverval将取消下次执行,进行下下次的方法调用。

function sayHi(phrase, who) {
    alert(phrase + ', ' + who);
}
setInterval(sayHi, 2000, "Hi", "Alison");//参数是函数名
setInterval('sayHi("Hi","Alison")', 2000);//参数是函数名

销毁方法:clearInterval(id); 创建定时器时会返回一个id

3)用setTimeout,不用setInterval
由于setInterval的执行时间不会等待方法执行时间,因此当方法执行时间大于设定时间时,会产生“丢帧”现象,从而导致不同定时器的代码执行间隔比预期小。

6.箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?

箭头函数是普通函数的简写,但是它不具备很多普通函数的特性。

  1. this指向问题:作为回调函数使用时不同(具体可见3.5)
  2. 获取参数:箭头函数没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符。
  3. 构造函数:普通函数可以作为构造函数来用,用new去新建对象实例。箭头函数不可以。
  4. prototype:箭头函数没有prototype原型,普通函数有。
  5. return:如果箭头函数仅有一个表达式,那么该表达式的结果会被隐式返回。而普通函数用return去返回一个函数的结果,若无return语句,或者return后面无表达式,则返回undefined。
7.js立即执行函数作用?

使用立即执行函数可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5尚未支持块级作用域,使用立即执行函数模拟块级作用域是相当普遍的。

  1. 将设置函数中的变量包裹在局部作用域中,不会泄露成全局变量。
  2. 只执行一次的设置函数
(function () {
	for (var i = 0; i < count; i++) {
        console.log(i);
	} 
})(); 
console.log(i); // 抛出错误:外部访问不到立即执行函数作用域中的变量i

传递参数:

(function (who) {
    console.log("I miss you, " + who)
})("kangkang")
(function (global) {
    console.log(global)
})(this)

通常全局变量被作为一个参数传递给立即执行参数,这样它在函数内部不使用window也可以被访问到。但不应该给立即执行函数传递太多的参数。

8.什么是原型?什么是原型链?

这部分我是看了一位大佬的文章之后敲敲代码豁然开朗的。此处附上链接:https://blog.csdn.net/cc18868876837/article/details/81211729

提到原型,初学的时候经常会被__proto__和prototype搞得十分混乱。__proto__一般被称为隐式原型,prototype一般被称为显式原型。

牢记:

  1. __proto__和constructor属性是对象独有的
  2. prototype属性是函数独有的

此处附上大佬画的图:
在这里插入图片描述
函数创建的对象.__proto__ ===该函数.prototype 该函数.prototype.constructor===该函数本身
1)__proto__隐式原型
__proto__属性是由一个对象指向一个对象,也就是指向他们的原型对象。
作用:就是当访问一个对象属性,该对象内部并不存在这个属性,他就会通过__proto__攀升原型链,找他的原型对象是否有该属性,直到原型链顶端null,然后报错。
通过__proto__属性向上攀升找原型对象直到null这样的一条链,就是我们所谓的原型链。我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠__proto__继承而来的。

2)prototype显式原型
prototype属性是由一个函数指向一个对象,也就是这个函数所创建的实例的原型对象(也可以理解为函数看作为对象)。因此f1.__proto__ === Foo.prototype
作用:包含所有实例化对象的公共属性和方法。
任何函数在创建的时候都会默认创建该函数的prototype对象。

3)constructor属性
consturctor属性是由一个对象指向一个函数,也就是指向这个对象的构造函数,其中Function对象比较特殊,他的构造函数就是他自己这个Function()方法,因此constructor属性的终点就是Function这个函数。
constructor这个属性来讲,只有prototype对象才有。其他对象都是通过__proto__继承prototype对象而得来的属性。

下面是敲代码验证:

function Foo() { };
let f1 = new Foo();

console.log(f1.__proto__);//是对象,是实例化对象f1指向f1的原型对象
console.log(Foo.prototype); //是对象,是Foo()这个函数指向Foo.prototype对象(类似于把Foo()函数看作是Foo对象,因为js里函数也是对象)
//函数创建的对象.__proto__===该函数.prototype
console.log(f1.__proto__ === Foo.prototype);//f1原型对象就是Foo.prototype这个函数对象

console.log(f1.constructor);//是函数,是由实例化对象f1指向该对象的构造函数Foo(),f1对象本身没有constructor属性,只有prototype对象才有,但是f1.__proto__===Foo.prototype,因此f1原型链上有constructor方法
console.log(Foo.prototype.constructor);//是函数,是由Foo.prototype对象指向该对象构造函数Foo()
console.log(f1.constructor === Foo);
//该函数.prototype.constructor===该函数本身
console.log(Foo.prototype.constructor === Foo);
9.js常用的继承方式有哪些?每个继承方式的优缺点?

原型继承、组合继承、寄生组合继承、es6新特性extends

1)原型继承
Object.create(作为新对象原型的对象[,给新对象定义额外属性的对象])
本质上是对传入的对象执行了一次浅拷贝(仅仅复制对象的引用)
优点:不需要单独创建构造函数,在对象间共享信息。
缺点:子类实例共享了父类构造函数的引用属性。

只有第一个参数:

let person = {
    name: "Alison",
    friends: ["Ruby", "Andy"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Yooo";
anotherPerson.friends.push("Robo");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.name);//Alison
console.log(person.friends);//['Ruby', 'Andy', 'Robo', 'Barbie']引用类型值共享

添加第二个参数:

let person = {
    name: "Alison",
    friends: ["Ruby", "Andy"]
};
let anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
console.log(anotherPerson.name);//Greg

2)组合继承
思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
优点:既可以把方法定义在原型上实现重用,又可以让每个实例有自己的属性。
缺点:效率问题:父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "green", "blue"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SubType(name, age) {
    SuperType.call(this, name); //继承属性,第二次调用
    this.age = age;
}
SubType.prototype = new SuperType(); //继承方法,第一次调用
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

let instance1 = new SubType("Yooo", 29);
instance1.colors.push("black");
console.log(instance1.colors);//['red', 'green', 'blue', 'black']
instance1.sayName();//Yooo
instance1.sayAge();//29

3)寄生组合继承
基本思路是不通过调用父类构造函数给子类原型赋值new SuperType(),而是取得父类原型的一个副本Object.create(SuperType.prototype)。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
优点:效率高,只调用了一次

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "green", "blue"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SubType(name, age) {
    SuperType.call(this, name); 
    this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

let instance1 = new SubType("Yooo", 29);
instance1.colors.push("black");
console.log(instance1.colors);//['red', 'green', 'blue', 'black']
instance1.sayName();//Yooo
instance1.sayAge();//29

4)extends
寄生组合继承的语法糖。子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话必须是super。

class Son extends Father { // Son.prototype.__proto__ = Father.prototype
    constructor(y) {
        super(200)  // super(200) => Father.call(this,200)
        this.y = y
    }
}
10.async await是做什么的?

async/await 是promise 的语法糖,可以让编写 promise 更清晰易懂。async/await 本质还是promise,使用更清晰的写法来替换 promise.then/catch 的方式。

1)async&await
async:在函数前加上async,函数将返回promise,我们就可以像使用标准Promise一样使用了。
await:await 用于替代 then 使编码更优雅,必须放在 async 定义的函数中使用,一般await后面是外部其它的promise对象。

async function hd(message) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(message);
        }, 2000);
    });
}

async function run() {
    let h1 = await hd("Yooo");
    console.log(h1);
    let h2 = await hd("Alison");
    console.log(h2);
}

run();

2)类中使用
和 promise 一样,await 也可以操作thenables 对象

class User {
  constructor(name) {
    this.name = name;
  }
  then(resolve, reject) {
    let user = ajax(`http://localhost:8888/php/user.php?name=${this.name}`);
    resolve(user);
  }
}
async function get() {
  let user = await new User("Yooo");
  console.log(user);
}
get();

类方法也可以通过 async 与 await 来操作promise

class User {
  constructor() {}
  async get(name) {
    let user = await ajax(
      `http://localhost:8888/php/user.php?name=${name}`
    );
    user.name += "-Yooo.com";
    return user;
  }
}
new User().get("Yooo").then(resolve => {
  console.log(resolve);
});

3)错误处理
async 内部发生的错误,会将promise对象变为rejected 状态,所以可以使用catch 来处理。

async function hd() {
  console.log(houdunren);
}
hd().catch(error => {
  throw new Error(error);
});

如果promise 被拒绝将抛出异常,可以使用 try…catch 处理错误。

async function get(name) {
  try {
    let user = await ajax(
      `http://localhost:8888/php/user.php?name=${name}`
    );
    console.log(user);
  } catch (error) {
    alert("用户不存在");
  }
}
get("向军老师");

多个 await 时当前面的出现失败,后面的将不可以执行。

4)并发执行
有时需要多个await 同时执行,有以下几种方法处理。
使用 Promise.all() 处理多个promise并行执行。

async function hd() {
  await Promise.all([p1(), p2()]);
}
hd();

让promise先执行后再使用await处理结果

async function hd() {
  let h1 = p1();
  let h2 = p2();
  await h1;
  await h2;
}
hd();
11.什么是变量提升,什么是函数提升,它们的优先级?

js预编译的时候会出现变量函数提升。
函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被变量赋值后覆盖。

1)变量提升
ES6之前,函数没有块级作用域(一对{}即一个块级作用域),只有全局作用域和函数作用域。变量提升是指将变量声明提升到它所在的作用域的最开始部分

console.log(foo); // undefined
var foo = '小花猫';
console.log(foo)  // 小花猫

相当于

var foo;
console.log(foo);
foo = '小花猫';
console.log(foo);

2)函数提升
函数创建有两种方式:
1、函数声明形式;function foo(){}
2、函数字面量形式(即函数表达式)。var foo = function(){}
(还有一种是方式:函数构造法:var a = new Fun(),技术角度来讲也是一个字面量形式。)
【而只有函数声明形式才有函数提升】

console.log(bar); // f bar() { console.log(123) }
bar(); // 123
var bar = 456;
function bar() {
    console.log(123);
}
console.log(bar); // 456
bar = 789;
console.log(bar); // 789
bar(); // bar is not a function

相当于

// 函数提升,函数提升优先级高于变量提升
var bar = function () {
    console.log(123)
};
// 变量提升,变量提升不会覆盖(同名)函数提升,只有变量再次赋值时,才会被覆盖
var bar;
console.log(bar); // f bar() { console.log(123) }
bar(); //123
// 变量赋值,覆盖同名函数字面量
bar = 456;
console.log(bar); //456
// 再次赋值
bar = 789
console.log(bar); //789
bar(); // bar is not a function,因为被同名变量覆盖了
12.let,var,const区别,TDZ暂时性死区是什么?
  • var
    • var声明的变量会进行变量提升,而let、const不会
    • var可以重复声明
  • let
    • let声明的变量有块级作用域,只在局部起作用,防止变量污染
    • let不可重复声明
  • const
    • 与let相同
    • 不可被改变,如果声明的是对象,是可以修改对象内部的值的
console.log(a);
let a = 1;
//报错:在初始化之前无法访问“a”

在作用域中let、const关键字声明的变量会先被创建出来,但是因为此时没有进行词法绑定(就是对声明语句进行求值运算),所以无法被访问,访问就会抛出错误。所以在变量在作用域中被创建到变量可以被访问这段时间,就称为TDZ暂时性死区。

13.什么是闭包?闭包的作用?闭包的应用?闭包会造成什么问题?

闭包指子函数可以访问外部作用域变量的函数特性。js中所有函数都是闭包,闭包一般在子函数本身作用域以外执行,即延伸作用域。

//使用闭包返回数组区间元素
let arr = [3, 2, 4, 1, 5, 6];
function between(a, b) {
  return function(v) {
    return v >= a && v <= b;
  };
}
console.log(arr.filter(between(3, 5)));

作用:

  1. 避免命名冲突
  2. 可以使用函数内部的变量,使变量不会被垃圾回收机制回收

应用:

  1. 设计模式中的单例模式
  2. for循环中的保留i的操作
  3. 防抖和节流
  4. 函数柯里化

问题:

  1. 内存泄漏
    闭包特性中上级作用域会为函数保存数据,从而造成的如下所示的内存泄漏问题。
<body>
  <div desc="houdunren">在线学习</div>
  <div desc="hdcms">开源产品</div>
</body>
<script>
  let divs = document.querySelectorAll("div");
  divs.forEach(function(item) {
    item.addEventListener("click", function() {
      console.log(item.getAttribute("desc"));
    });
  });
</script>

上面item会多次创建,占用内存,最终导致内存泄漏。
通过清除每次循环中item解决内存泄漏问题。

let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
  let desc = item.getAttribute("desc");
  item.addEventListener("click", function() {
    console.log(desc);
  });
  item = null;
});
  1. this指向
    this 总是指向调用该函数的对象(除箭头函数外),即函数在搜索this时只会搜索到当前活动对象。下面是函数因为是在全局环境下调用的,所以this指向window,这不是我们想要的。
let hd = {
  user: "后盾人",
  get: function() {
    return function() {
      return this.user;
    };
  }
};
console.log(hd.get()()); //undefined

使用箭头函数解决这个问题

let hd = {
  user: "后盾人",
  get: function() {
    return () => this.user;
  }
};
console.log(hd.get()()); //后盾人
14.谈谈事件循环?
15.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值