面试:JavaScript基础概念

文章目录

  • 1. JS基础概念
    • JavaScript 常见知识总结
    • 重点
      • 1.js的基本数据类型都有哪些
      • 2.判断基本数据类型的方法
        • typeof
        • instanceof
        • Object.prototype.toString.call()
      • 3.基本数据类型和引用数据类型的区别
        • 基本数据类型
        • 引用数据类型
        • 栈内存和堆内存的优缺点
      • 4.原型和原型链
        • 创建没有原型的对象的方法
        • 解释一下原型链
        • 函数的prototype
      • 5.继承
        • 继承属性(构造函数)
        • 继承方法
        • hasOwnProperty
          • 遍历一个对象上自身不包括原型链上的属性
        • ES6继承
        • 继承的实现方式及比较(问题及解决)
      • 6.class
        • 类继承
        • super
      • 7.解释一下闭包
        • 闭包函数保存变量会造成内存泄漏吗?如何解决
        • 内存泄漏
        • 引用计数
        • 标记清除
      • 8.JS事件循环:宏任务、微任务、执行顺序
        • JS为什么是单线程的
        • 宏任务(macro task)
        • 微任务(micro task)
        • DOM渲染的时机
      • 9.变量提升
        • 为什么有变量提升
        • var和function变量提升的优先级
        • 变量声明
      • 10.call(), apply(), bind()区别
      • 11.this指向
        • 1. 默认绑定(Default Binding)
        • 2. 隐含绑定(Implicit Binding)
          • 隐含丢失(Implicitly Lost)
        • 3. 明确绑定(Explicit Binding)
          • 硬绑定(Hard Binding)
        • 4. new 绑定(new Binding)
        • 5. 绑定优先级顺序
        • 6.判定 this
        • 7.特例:被忽略的 this
        • 8.ES6: 箭头函数的词法 this
        • 一些题目
      • 12.ES6、7、8新特性
        • ES6
          • 类的支持(class关键字)
          • 解构赋值基本用法
        • ES7
        • ES8
        • ES9
      • 13.事件委托(代理)/ 事件冒泡 / 事件捕获
        • 事件代理(委托)
        • 事件冒泡
      • 14.DOM事件模型
        • 1.原始事件模型(DOM0级)
        • 2.DOM2事件模型
        • 3.在当前的事件模型中,哪些事件可以冒泡,哪些不会冒泡,为什么?不会冒泡的怎么实现事件委托
      • 15.异步编程/解决方案
        • 1.async/await 对它有什么认识,还有它的问题?
        • 2.ES6 Generator 函数
        • 3.async/defer标签
        • 4.回调函数
        • 发布/订阅者模式
      • 16.Promise
        • 错误捕获
      • 17.ES6:let var const区别
        • 暂时性死区
        • 注意要点
      • 为什么let和const不能重复声明
      • 18.箭头函数和普通函数区别
        • 使用场景
      • 19.作用域:词法作用域和动态作用域
        • 词法作用域:JavaScript 所采用的作用域模型
        • 动态作用域:JavaScript没有动态作用域
        • 作用域链(Scope Chain)
        • 执行环境(execution context)
    • 其他
      • 1.JS执行
      • 2.Symbol / BigInt
        • Symbol
          • 基本用法
          • 应用场景
        • BigInt
      • 2.数组的方法都有哪些
      • 3.map和forEach区别
        • 相同点
        • 不同点
      • 4.Set/Map/WeakSet/WeakMap
      • 5.类型转换
      • 6.模块化:什么是amd、commonjs、umd、esm?
      • 7.for in/for of区别
      • 8.面向对象面向的三大基本特征,五大基本原则
      • 9.JSON.stringify()和JSON.parse()应用场景
      • 10.JS编译过程
      • 11. sort()用的什么排序方式
      • 12.深拷贝/浅拷贝
        • JSON.parse(JSON.stringify(obj))的问题
      • 13.requestAnimationframe
      • 14.forEach跳出循环
      • 15.检测空对象
      • 16.if-else/switch
      • 17.数组/对象遍历方法
        • 数组遍历
        • 对象遍历
      • 18.二进制/十进制互相转换
      • 19.面向对象写js有什么优缺点
      • 20.parseInt和number的区别
        • parseInt()
        • Number()
      • 监听一个元素的变化
      • JS中0.1+0.2是多少,如何解决精度问题
      • 单精度/双精度
        • 单精度
        • 双精度
      • setTimeout, setInterval
      • 懒加载是怎么完成的(图片and组件)
      • DOM事件流
      • 垃圾回收
      • 如果一个发起了两个请求,第二个请求先返回,第一个请求后返回,这种情况下怎么保证展示最新数据?
      • for循环 + setTimeout
      • CommonJS与ES6模块的差异
      • ==和 === 以及 Object.is() 的区别
        • ==判断流程
      • 图片上传

1. JS基础概念

JavaScript 常见知识总结

JavaScript 常见知识总结

重点

1.js的基本数据类型都有哪些

JavaScript 定义了七种内建类型:

  • null
  • undefined
  • boolean
  • number
  • string
  • object
  • symbol – 在 ES6 中被加入的!

注意: 除了 object ,所有这些类型都被称为“基本类型(primitives)”。

2.判断基本数据类型的方法

typeof

typeof 操作符可以检测给定值的类型,而且总是返回七种字符串值中的一种

typeof undefined     === "undefined"; // true
typeof true          === "boolean";   // true
typeof 42            === "number";    // true
typeof "42"          === "string";    // true
typeof { life: 42 }  === "object";    // true

// 在 ES6 中被加入的!
typeof Symbol()      === "symbol";    // true

例外null类型

typeof null === "object"; // true

//正确方法
var a = null;

(!a && typeof a === "object"); // true

原因:这是个历史遗留bug。
JS类型值是存在32 BIT 单元里,32位有1-3位表示TYPE TAG,其它位表示真实值
表示object的标记位正好是低三位都是0
1:整型(int)
000:引用类型(object)
010:双精度浮点型(double)
100:字符串(string)
110:布尔型(boolean)
另外还用两个特殊值:
undefined,用整数−2^30(负2的30次方,不在整型的范围内)
null,机器码空指针(C/C++ 宏定义),低三位也是000

typeof 可以返回的第七种字符串值是function

typeof function a(){ /* .. */ } === "function"; // true

数组是一个object

typeof [1,2,3] === "object"; // true
  • 对于引用类型,typeof始终会返回一个"object",在JS中还有内置对象(JavaScript 标准内置对象),我们无法分清楚他们具体是哪个对象,所有instanceof出现了。
instanceof

1.instanceof方法能准确的识别变量的所属具体对象
instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
语法
object instanceof constructor
描述
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上

typeof和instanceof都可以用来判断变量,它们的用法有很大区别
typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值

var num = new Number(10);
console.log(num instanceof Number);//返回true

var arr = new Array(1,2,3);
console.log(arr instanceof Array); // true

通俗来讲,instanceof就是判断变量是否属于某种类型。

Object.prototype.toString.call()

在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。但是它不能检测非原生构造函数的构造函数名
要想区分对象、数组、函数、单纯使用typeof是不行的。在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。
分为Null、String、Boolean、Number、Undefined、Array、Function、Object、Date、Math

  1. 判断基本类型
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
  1. 判断原生引用类型
**函数类型**
Function fn(){
  console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(person); // "[object Object]"

很明显这种方法不能准确判断person是Person类的实例,而只能用instanceof 操作符来进行判断,如下所示:

console.log(person instanceof Person); // true
  1. 判断原生JSON对象
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);// 输出结果为”[object JSON]”说明JSON是原生的,否则不是

3.基本数据类型和引用数据类型的区别

基本数据类型
  • null
  • undefined
  • boolean
  • number
  • string
  • symbol – 在 ES6 中被加入的!

基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问和操作。
面试:JavaScript基础概念_第1张图片

引用数据类型

object :

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function
  • Date
  • RegExp
  • Error

引用数据类型是保存在堆内存中的对象不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。
面试:JavaScript基础概念_第2张图片

var a = [1,2,3,4,5];
var b = a;//传址 ,对象中传给变量的数据是引用类型的,会存储在堆中;
var c = a[0];//传值,把对象中的属性/数组中的数组项赋值给变量,
//这时变量C是基本数据类型,存储在栈内存中;改变栈中的数据不会影响堆中的数据
alert(b);//1,2,3,4,5
alert(c);//1
//改变数值 
b[4] = 6;
c = 7;
alert(a[4]);//6
alert(a[0]);//1

这就是传值传址的区别。因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(相当于新建了一个不同名“指针”),而不是堆内存中的对象。而c仅仅是从a堆内存中获取的一个数据值,并保存在栈中。所以b修改的时候,会根据地址回到a堆中修改,c则直接在栈中修改,并且不能指向a堆内存中。

栈内存和堆内存的优缺点

在JS中,基本数据类型变量大小固定,并且操作简单容易,所以把它们放入栈中存储。
引用类型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存由于它的特点,所以它的系统效率较高。
堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。

4.原型和原型链

无论什么时候,只要创建了一个函数,就会根据为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会获得一个 constructor,该属性是一个指向 prototype 属性所在函数的指针。

原型链规定了对象如何查找属性,对于一个对象来说,如果它本身没有某个属性,则会沿着原型链一直向上查找,知道找到属性或者查找完整个原型链。

原型链是实现继承的主要方法,其基本思想是利用原型链让一个引用类型继承另一个引用类型的属性和方法

创建没有原型的对象的方法

obj = Object.create(null)
创建一个没有任何属性的对象

obj = {}会创建一个从Object继承属性和方法的对象(原型为Object)

解释一下原型链

作用:查找引用类型的属性

在JavaScript中,对象内部都有一个原型对象,当一个对象查询某个属性或方法的时候,JavaScript引擎首先会搜索对象本身是否存在答案,如果没有,就会去它的原型对象中继续搜索,如果没有,再去它的原型的原型中去找,这就形成了一个原型链。直到Object的prototype为止

在对象使用属性或调用方法的时候,会优先在自身的属性中寻找,如果找不到就去隐式原型__proto__里面依次寻找,如果找不到就返回null,我们把__proto__ 与prototype 的链条关系称为“原型链”。js对象就是通过原型链,实现属性的继承。
面试:JavaScript基础概念_第3张图片
实例对象的__proto__指向构造函数的prototype,从而实现继承。

prototype对象相当于特定类型所有实例对象都可以访问的公共容器

面试:JavaScript基础概念_第4张图片

function Person() {} //创建构造函数

var person = new Person(); //使用 new 创建一个实例对象 person

console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
console.log(person.constructor === Person); // true
//当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的(隐式)原型也就是 person.__proto__(Person的显式原型Person.prototype) 中读取,正好原型中有该属性,所以:
console.log(person.constructor === Person.prototype.constructor);  true

var obj = new Object();
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

// ES5方法,获得对象的原型
//__proto__属性并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,
//不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
console.log(obj.__proto__ === Object.getPrototypeOf(obj)); // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

面试:JavaScript基础概念_第5张图片

  • Person构造函数。构造函数本身就是一个函数,与普通函数没有任何区别,不过为了规范一般将其首字母大写。构造函数和普通函数的区别在于,使用new生成实例的函数就是构造函数,直接调用的就是普通函数
  • Person.prototype实例原型,也叫原型对象,包含所有实例共享的属性和方法。prototype表示该函数的显式原型,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例(person)的原型(person.__proto__。这个属性是原型对象⽤来创建新对象实例,⽽所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性
  • person实例(对象)。通过new调用Person构造函数所创建的一个实例对象。
  • person.__proto__隐式原型。这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这是一个访问器属性(即 getter 函数和 setter 函数),会指向该对象的原型构造该对象的构造函数的原型)。通过它可以访问到对象的内部[[Prototype]] (一个对象或 null )。
  • constructor:返回创建实例对象时构造函数的引用,即返回实例原型的构造函数的引用。此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。
  • Object()Object 构造函数,创建一个对象包装器,会根据给定的参数创建对象(实例)。在 JavaScript 中,几乎所有的对象都是 Object 类型的实例,它们都会从 Object.prototype 继承属性和方法。
  • Object.prototype():原型对象就是通过Object构造函数生成的,实例的 __proto__ 指向构造函数的 prototype ,可以理解成,Object.prototype()所有对象的根对象

每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,因为Object.prototype 的原型是null。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。

当一个对象想要查找某个方法或属性时,如果对象本身没有,则会去对象的原型对象中查找,一层层往上查找,直到Object.prototype

小结:

  • 使用new生成实例的函数就是构造函数,直接调用的就是普通函数;
  • 每个对象都拥有一个原型对象;
  • 每个引用类型的隐式原型都指向它的构造函数的显式原型;
  • Object.prototype是所有对象的根对象;
  • 原型链存在终点null,不会无限查找下去;
函数的prototype

面试:JavaScript基础概念_第6张图片

typeof Function // "function"
typeof Function.prototype // "function"

第一行的结果,我们大家都能理解,第二行的运行结果大家估计就够呛了,按照往常的理解,构造函数的原型肯定是对象;例如:var fn=function(){};

var fn=function(){};
typeof fn; // "function"
typeof fn.prototype; // "object"

5.继承

继承是指一个对象直接使用另外一个对象的属性和方法

继承属性(构造函数)

先创建一个Person

function Person (name, age) {
    this.name = name
    this.age = age
}

// 方法定义在构造函数的原型上
Person.prototype.getName = function () { 
	console.log(this.name)
}

想创建一个Teacher类,我希望它可以继承Person所有的属性,并且额外添加属于自己特定的属性:subject——这个属性包含了教师教授的学科。

定义Teacher的构造函数

function Teacher (name, age, subject) {
    Person.call(this, name, age)
    this.subject = subject
}

属性的继承是通过在一个类内执行另外一个类的构造函数,通过call指定this为当前执行环境,这样就可以得到另外一个类的所有属性。

Person.call(this, name, age)
继承方法

方法都定义在prototype里,那其实我们只需要把Person.prototype的备份赋值给Teacher.prototype即可

Teacher.prototype = Object.create(Person.prototype)

Object.create()方法创建一个新对象(new Teacher()),使用现有的对象来提供新创建的对象的__proto__,即构造函数Teacher()创建的实例的__proto__,即Teacher.prototype

为什么是备份呢?

因为如果直接赋值,那会是引用关系,意味着修改Teacher. prototype,也会同时修改Person.prototype,这是不合理的。

另外注意一点就是,在给Teacher类添加方法时,应该在修改prototype以后,否则会被覆盖掉,原因是赋值前后的属性值是不同的对象。

最后还有一个问题,我们都知道prototype里有个属性constructor指向构造函数本身,但是因为我们是复制其他类的prototype,所以这个指向是不对的,需要更正一下。
如果不修改,会导致我们类型判断出错

Teacher.prototype.constructor = Teacher

继承方法的最终方案:

Teacher.prototype = Object.create(Person.prototype)
Teacher.prototype.constructor = Teacher
hasOwnProperty

在原型链上查询属性比较耗时,对性能有影响,试图访问不存在的属性时会遍历整个原型链。

遍历对象属性时,每个可枚举的属性都会被枚举出来。 要检查是否具有自己定义的属性,而不是原型链上的属性,必须使用hasOwnProperty方法。

hasOwnProperty 是 JavaScript 中唯一处理属性并且不会遍历原型链的方法。

遍历一个对象上自身不包括原型链上的属性
//遍历对象和子对象
var obj = {
	name: "zhang" ,
	age: 18,
	son : {
		name: "wang",
		age: 2
	}
}

for(var prop in obj){
	//排除原型链上的属性
	if(obj.hasOwnProperty(prop)) {
		console.log(obj[prop]);
	}
}
ES6继承
// 父类
class Person {
    constructor(name, age) {
        console.log(`构造函数执行了,${name}`);
        this.name = name;
        this.age = age;
    }
    getPersonName() {
        return `名字为${this.name}`;
    }
}
let p1= new Person('jona');
console.log(p1.getPersonName());
// 子类
class Teacher extends Person {
    constructor(args, subject) {
	    // super() 指的是调用父类
	    // 调用的同时,会绑定 this 。
	    // 如:Foo.call(this, who)
        super(args);
        this.subject = subject;
    }
 	getTeacherName(){
        return super.getPersonName();//调用父级的方法也是用super
    }
}
let p2 = new Teacher('子类');
console.log(p2.getTeacherName());

先创建父类,然后通过extends关键字创建子类,通过super关键字将父类的属性和方法继承到子类.

继承的实现方式及比较(问题及解决)
  1. 简单的原型继承
function SuperType() {
    this.name = "super"
}
function SubType() {}

// 利用原型链实现继承
SubType.prototype = new SuperType()

var instance1 = new SubType()
console.log(instance1.name) // super

简单的原型继承存在以下两个问题:

  • 包含引用类型值的原型属性会被所有实例共享,在通过原型来实现继承时,原型实际上也会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性。
  • 在创建子类类型的实例时,不能向超类类型的构造函数中传递参数
  1. 借用构造函数继承(经典继承)
    这种继承的思想是在子类的构造函数内部调用超类的构造函数,该方法使用 call()apply() 方法在新创建的对象上执行构造函数。
function SuperType(age, name) {
    this.colors = ["blue", "red"]
    this.age = age
    this.name = name
}
function SubType() {
    SuperType.call(this, ...arguments)
}

var instance1 = new SubType(23, "sillywa")
instance1.colors.push("yellow")
console.log(instance1.colors, instance1.name)

var instance2 = new SubType(12, "xinda")
console.log(instance2.colors, instance2.name)

借用构造函数继承也有一些缺点,比如方法都只能在构造函数中定义,没有办法实现方法的复用

  1. 组合式继承
    组合继承结合了原型继承和借用构造函数继承的优点,其背后的思想是,使用原型链实现对原型方法的继承,使用构造函数实现对实例属性的继承
function SuperType(name) {
    this.name = name
    this.colors = ["red", "yellow"]
}
// 方法写在原型上
SuperType.prototype.sayName = function() {
    return this.name
}
function SubType(name, age) {
    // 通过 构造函数继承属性
    SuperType.call(this, name)
    this.age = age
}
// 通过原型继承方法
SubType.prototype = new SuperType()

// 重写了 SubType 的 prototype 属性,因此其 constructor 也被重写了,需要手动修正
SubType.prototype.constructor = SubType

// 定义子类自己的方法
SubType.prototype.sayAge = function() {
    return this.age
}
  1. 原型式继承
    借助原型可以通过已有的对象创建新对象,同时还不必因此创建自定义类型。为达到这个目的,可以定义如下函数:
function create(o) {
    function F(){}
    F.prototype = o
    return new F()
}

在传入一个参数的情况下,这个函数行为等同于Object.create()

Object.create() 方法的第二个参数与 Object.defineProterties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:

var person = {
    name: "sillywa"
}
var person1 = Object.create(person, {
    name: {
        value: "John"
    }
})
console.log(person1.name)        // John
  1. 寄生式继承
    寄生式继承的思路与继承构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正地是它做了所有工作一样返回对象。以下是寄生式继承的代码:
function createAnother(original) {
    var clone = Object.create(original)
    clone.sayHi = function() {
        console.log("Hi")
    }
    return clone
}
  1. 组合寄生式继承
    前面说过,组合继承是 JavaScript 最常用的继承模式,不过它也有自己的缺点,组合继承最大的问题是,无论什么情况下都会调用两次超类的构造函数。
    组合寄生式继承就是为了解决这一问题,将第二次调用构造函数改为使用 Object.create() 函数来实现。
    见上方继承。

6.class

在 ES6 规范中,引入了 class 的概念。使得 JS 开发者终于告别了,直接使用原型对象模仿面向对象中的类和类继承时代。

但是JS 中并没有一个真正的 class 原始类型class 仅仅只是对原型对象运用语法糖

typeof className == "function"

通过类型判断,我们可以得知,class 的并不是什么全新的数据类型,它实际只是 function (或者说 object)。

class 定义属性
当我们使用 class 定义属性(方法)的时候,实际上等于是在 class 的原型对象上定义属性。

class Foo {
  constructor(){ /* constructor */  }

  describe(){ /* describe */  }
}

// 等价于
function Foo (){
  /* constructor */
}

Foo.prototype.describe = function(){  /* describe */  }

constructor 是一个比较特殊的属性,它指向构造函数(类)本身。可以通过以下代码验证。

Foo.prototype.constructor === Foo // true
类继承

在传统面向对象中,类是可以继承类的。这样子类就可以复制父类的方法,达到代码复用的目的。

ES6 也提供了类继承的语法 extends,如下:

class Foo {
  constructor(who){
    this.me = who;
  }
  //静态方法
  static displayName = "Foo";
  static identity() {
    return "I am " + this.me;
  }
  
  identify(){
    return "I am " + this.me;
  }
}


class Bar extends Foo {
  constructor(who){
    // super() 指的是调用父类
    // 调用的同时,会绑定 this 。
    // 如:Foo.call(this, who)
    super(who);
  }

  speak(){
    alert( "Hello, " + this.identify() + "." );
    console.log(this.identify === super.identify);// 这里的super和this等价
  }
}

Foo.displayName // "Foo"

var b1 = new Bar( "b1" );

b1.speak();

如果子类中定义了构造函数,那么它必须先调用 super() 才能使用 this 。

当实例 b1 调用 speak 方法时,b1 本身没有 speak,所以会到 Bar.prototype 原型对象上查找,并且调用原型对象上的 speak 方法。调用 identify 方式时,由于 this 指向的是 b1 对象。所以也会先在 b1 本身查找,然后沿着原型链,查找 Bar.prototype,最后在 Foo.prototype 原型对象上找到 identify 方法,然后调用。

面试:JavaScript基础概念_第7张图片

super
  1. this和super的区别:
  • this关键词指向函数所在的当前对象
  • super指向的是当前对象的原型对象
  1. Class中的 super()
  • 它在这里表示父类的构造函数,用来新建父类的 this 对象,super()相当于Parent.prototype.constructor.call(this)
  • 子类没有自己的this对象,而是继承父亲的this对象,然后进行加工。如果不调用super,子类就得不到this对象
  • super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象
class Parent {
	static myMethod(msg) {
		console.log('static', msg);
	}
	myMethod(msg) {
		console.log('instance', msg);
	}
}
class Child extends Parent {
	static myMethod(msg) {
		super.myMethod(msg);
	}
	myMethod(msg) {
		super.myMethod(msg);
	}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

7.解释一下闭包

作用:

  • 可以读取函数内部的变量
  • 让这些变量的值始终保持在内存中

闭包就是能够读取其他函数内部变量的函数

作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中

  1. 闭包可以用来隐藏变量,避免全局污染。
  2. 也可以用于读取函数内部的变量。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数,且封闭了定义时的环境"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

function foo() {
  console.log(a);
}

function bar() {
  let a = 3;
  foo();
}

let a = 2;

bar(); // 2

之所以输出 2,是因为 foo 是一个闭包函数

闭包就是 函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。

前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数,即 这个函数封闭了它自己的定义时的环境,形成了一个闭包,所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。

function foo() {
	var a = 2;

	function bar() {
		console.log( a );
	}

	return bar;
}

var baz = foo();

baz(); // 2 -- 哇噢,看到闭包了,伙计。

有赖于它被声明的位置,bar() 拥有一个词法作用域闭包覆盖foo()内部作用域,闭包为了能使 bar() 在以后任意的时刻可以引用这个作用域而保持它的存在。

bar() 依然拥有对那个作用域的引用,而这个引用称为闭包

所以,在几微秒之后,当变量 baz 被调用时(调用我们最开始标记为 bar 的内部函数),它理所应当地对编写时的词法作用域拥有 访问权,所以它可以如我们所愿地访问变量 a。

这个函数在它被编写时的词法作用域之外被调用。闭包 使这个函数可以继续访问它在编写时被定义的词法作用域

无论我们使用什么方法将内部函数 传送 到它的词法作用域之外,它都将维护一个指向它最开始被声明时的作用域的引用,而且无论我们什么时候执行它,这个闭包就会被行使。

闭包:一个函数声明时,对自己的声明时的作用域进行一个打包并绑定,无论这个函在词法作用域内部还是外部执行,其引用的作用域一直为自己定义时的作用域。而这个引用称为闭包

块作用域和闭包携手工作:

for (let i=1; i<=5; i++) {
	setTimeout( function timer(){
		console.log( i );
	}, i*1000 );
}

在用于 for 循环头部的 let 声明被定义了一种特殊行为。这种行为说,这个变量将不是只为循环声明一次,而是为每次迭代声明一次。并且,它将在每次后续的迭代中被上一次迭代末尾的值初始化。

闭包函数保存变量会造成内存泄漏吗?如何解决

当然,闭包的作用域链中保存的元素,该元素将无法被销毁,在垃圾回收时不会被收回。如果保存元素为一个引用变量,而且不是必须要保存的,那么它也会因此被保存下来占据大量的内存,造成内存泄漏。

如何避免闭包引起的内存泄漏

  1. 在退出函数之前,将不使用的局部变量全部删除。可以使变量赋值为null,及时解除引用(闭包中的变量,dom引用,定时器清除);
  2. 通过解除对匿名函数的引用,可以将匿名函数占用的内存安全释放。
  3. 利用匿名函数(立即执行函数),制造私有作用域(块级作用域),这样匿名函数执行完之后可以将引用的活动对象销毁。(function() {})()立即执行函数,也称为小闭包。
  4. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。

闭包和闭包引起的内存泄露

内存泄漏

JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用。对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

如何识别内存泄漏

  • 意外的全局变量:(good: const bar = ‘’;)、(bad: bar = ‘’; this.bar = ‘’);解决:使用严格模式避免
  • 闭包:活动对象被引用,使闭包内的变量不会被释放;解决: 将活动对象赋值为null
  • 被遗忘的计时器和回调函数: 如果回调函数内没有做什么事情,并且也没有被 clear 掉的话,就会造成内存泄漏。定时器内部实现闭包,回调也是闭包。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收.setTiemout 也会有同样的问题。所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout以及设置变量为null。
  • 被清理的DOM元素的引用:当我们需要多次访问同一个 DOM 元素时,一个好的做法是将 DOM 元素用一个变量存储在内存中,因为访问 DOM 的效率一般比较低,应该避免频繁地反问 DOM 元素。
    删除DOM元素时,document.body.removeChild(document.getElementById('button'))
    **虽然这样看起来删除了这个 DOM 元素,但这个 DOM 元素仍然被 button 这个变量引用,所以在内存上,这个 DOM 元素是没法被回收的。**所以在使用结束后,还需要将 button 设成 null。

用weakMap/weakSet存储dom节点最后可以被回收,防止内存泄漏。

引用计数

引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需要了。

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。
问题:循环引用,如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露;
解决:手动解除引用

标记清除

当变量进入执行环境时,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

JavaScript 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象…对这些活着的对象进行标记,这是标记阶段。清除阶段就是清除那些没有被标记的对象。

工作流程:

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
    面试:JavaScript基础概念_第8张图片

问题:

  1. 效率较低;因为在标记-清除阶段,整个程序将会等待,所以如果程序出现卡顿的情况,那有可能是收集垃圾的过程。

  2. 在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一边移动,最后清理掉边界的内存。不过可以想象,这种做法的效率没有标记-清除高。

8.JS事件循环:宏任务、微任务、执行顺序

JS为什么是单线程的

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

js 是单线程执行的,js中的任务按顺序一个一个的执行。如果一个任务耗时过长,后边一个任务也得等着,因此将任务分为了同步任务异步任务;而异步任务又可以分为微任务宏任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,像页面骨架和页面元素的渲染,而加载图片音乐之类的任务就是异步任务
同步任务在主线程上执行,异步任务放在主线程之外的一个任务队列。主线程执行完毕后,读取任务队列的内容。

面试:JavaScript基础概念_第9张图片
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

宏任务(macro task)

当前主线程上执行的就是一个宏任务。例: 整体script 的代码、setTimeout、setInterval、postMessage、I/O、html解析过程、获取资源(fetch、XMLHttpRequest)、DOM操作的响应UI Rendering等。

微任务(micro task)

例:Promise.then catch finally(注意不是说 Promise,new promise直接执行)、process.nextTick、await后面的代码。process.nextTick优先级最高,总是最先执行。

JS异步还有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入宏任务的event queue,然后再执行微任务,将微任务放入微任务的eventqueue,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回调函数,然后再从宏任务的queue拿宏任务的回调函数,如下图:

面试:JavaScript基础概念_第10张图片

首先执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue

当前 宏任务 执行完毕后,会先查看微任务队列,如果有任务,优先执行,否则执行下一个宏任务。

上述过程会不断重复,也就是常说的Event Loop(事件循环)。

在这里插入图片描述

  • 一个宏任务 + 当前宏任务执行环境下的所有微任务】为一个事件循环。
  • 第一个宏任务为script(整体代码)下的任务
  • Promise的then/catch/finally之前的内容,为宏任务
  • 多层Promise嵌套,嵌套的会排在没有嵌套的Promise后面
 async function async1() {
  console.log("async1 start");//2
  // 第三个宏任务
  await setTimeout(() => {
    console.log("async1 end");//9
    new Promise(function (reslove) {
      reslove();
    }).then(function () {
    // 第三个宏任务中的第一个微任务
      console.log("promise4");//10
    });
  }, 0);
  async2();//第一个宏任务中的第一个微任务
}
async function async2() {
  console.log("async2");// 5
}
//--------------------------------上面是函数的声明

console.log("script start");// 第一个宏任务(script)1

// 第二个宏任务
setTimeout(() => {
  console.log("setTimeOut");// 7
  new Promise(function (reslove) {
    reslove();
  }).then(function () {
  // 第二个宏任务中的第一个微任务
    console.log("promise3"); //8
  });
}, 0);

async1();//在第一个宏任务下执行

//在第一个宏任务下执行
new Promise(function (reslove) {
  console.log("promise1");//正常执行 3
  reslove();
}).then(function () {
  console.log("promise2");//第一个宏任务下的第二个微任务 6
});
console.log("script end");// 4

宏任务和微任务到底是什么?

DOM渲染的时机

渲染是微任务完成之后执行:
不要在.then()中写耗时的循环,会影响后续Dom渲染以及宏任务的释放。

微任务和宏任务的区别

  • 宏任务:DOM渲染后触发,由浏览器规定(Web APIs)

  • 微任务:DOM渲染前执行,微任务是ES6语法规定

宏任务 -> 微任务 -> DOM渲染 -> 下一轮宏任务 -> …

9.变量提升

  • JavaScript 中,函数及变量的声明都将被提升到函数的最顶部

JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。

以下两个实例将获得相同的结果:

a = 2;
var a;

console.log(a);//输出2
var a;
a = 2;

console.log(a);//输出2

变量提升(hoisting)会将当前作用域的所有变量的声明提升到程序的顶部。

  • js会将变量的声明提升到顶部,可是赋值语句并不会提升
console.log(a);//输出undefined

var a = 2;

var a = 2是分为两步的:

var a;
a = 2;

而js只会将第一步提升到顶部,所以上面的语句等价于:

var a;

console.log(a);

a = 2;
为什么有变量提升

js和其他语言一样,都要经历编译和执行阶段。而js在编译阶段的时候,会搜集所有的变量声明并且提前声明变量,而其他的语句都不会改变他们的顺序,因此,在编译阶段的时候,第一步就已经执行了,而第二步则是在执行阶段执行到该语句的时候才执行。

那为什么会进行变量提升呢?主要有以下两个原因:

(1)提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了

(2)容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;
var a;
console.log(a);

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,我们在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
var和function变量提升的优先级

二者优先级:函数提升会优先于变量提升,而且不会被同名的变量覆盖,但是,如果这个同名变量已经赋值了,那函数变量就会被覆盖。当二者同时存在时,会先指向函数声明

console.log(a);    //f a() {...}
console.log(a());    //2
var a = 1;
function a() {
  console.log(2);  
}
console.log(a);    //1
a = 3;
console.log(a());    //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()

上述代码相当于如下:

function a() {
  console.log(2);  
}
var a;
console.log(a);    //f a() {...}
console.log(a());    //2
a = 1;
console.log(a);    //1
a = 3;
console.log(a());    //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()
变量声明
  • 临时性死区
    它是变量提升的一种特殊情况,因为无论你在块的哪一个地方利用let声明了一个变量,都会产生一个从块的开始部分到该变量声明语句的临时性死区。

比较安全可靠:对var或者是直接声明全局变量来说,变量都可以未声明或者在声明语句之前就使用,而使用了let之后,该变量必须在其声明语句后,才能使用,否则就会报错。这就在一定程度上避免了变量滥用的情况。

var i = 5;   // j的临时性死区
(function hh() { 
  var i
  console.log(i) // undefined
  i = 10
})() // j的临时性死区
// j的临时性死区
let j = 55; // 接下来可以愉快的使用let了
console.log(j)
console.log(j+10)
(function hhh() {
  console.log(j) // 新的j的临时性死区
  let j = 77 //又有一个声明语句,从这个函数的开始部分到这里,都是新的j的临时性死区
})()
  • 当函数声明与其他声明一起出现的时候,是以谁为准呢?答案就是,函数声明高于一切,毕竟函数是js的第一公民。
foo();

function foo() {
    console.log('foo');//输出foo
}
var foo = 2;
  • 有多个函数声明的时候,是由最后面的函数声明来替代前面的
foo();

function foo() {
    console.log('1');
}

function foo() {
    console.log('2');//输出2
}

函数声明会被提升,但是函数表达式不会。

foo();

var foo = function() {
    console.log('foo');//输出TypeError
}

等价于:

var foo;

foo(); // TypeError
bar(); // ReferenceError

foo = function() {
	var bar = ...self...
	// ...
}

它其实也是分为两部分,一部分是var foo,而一部分是foo = function() {}。这道题的结果应该是报了TypeError(因为foo声明但未赋值,因此foo是undefined)。

10.call(), apply(), bind()区别

callbindapply 这三个函数的第一个参数都是 this 的指向对象,用来改变函数执行时的this指向

  • call(): 通过 foo.call(obj) 使用 明确绑定 来调用 foo,允许我们强制函数的 this 指向 objcall 的参数是直接放进去的,通过逗号隔开。
  • apply(): apply 的所有其他参数都必须放在一个数组或类数组里面传进去。
    注意: 就 this 绑定的角度讲,call(..)apply(..) 是完全一样的。
  • bind()bind(..) 返回一个硬编码的新函数,它使用你指定的 this 环境来调用原本的函数,需要重新调用执行这个新函数,而 applycall 则是立即调用。。它的参数传递和 call 一样。
    注意: 如果 bind 的第一个参数是 null 或者 undefinedthis 就指向全局对象 window
function foo(a, b, c) {
    console.log(this.a, a, b, c);
    return this.a + a + b + c;
}

var obj = {
    a: 2
};

var b = foo.call(obj, 3, 4, 5); // 2 3 4 5
console.log(b); // 14

var c = foo.apply(obj, [3, 4, 5]); // 2 3 4 5
console.log(c); // 14

var baz = foo.bind(obj, 3, 4, 5);
var d = baz(3); // 2 3 4 5
console.log(d); // 14

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    // 创建新属性
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
	// sonFn.prototype的原型为fatherFn.prototype
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.__proto__ === supFather.prototype // true
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,可以在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18} 

11.this指向

var name = '小红';
var b = {
	name: '小白',
	detail: function(){
		console.log(this.name);
	}
}
b.detail(); //小白
var c = b.detail;
c(); //小红

函数调用:指向window
引用调用:指向obj
面试:JavaScript基础概念_第11张图片

this 不是编写时绑定,而是运行时绑定。 它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。

this 既不是函数自身的引用,也不是函数 词法 作用域的引用。

this 实际上是在函数被调用时建立的一个绑定,它指向 什么 是完全由函数被调用的调用点来决定的。

1. 默认绑定(Default Binding)

独立函数调用,全局作用域中声明变量/调用函数。这种 this 规则是在没有其他规则适用时的默认规则。

2. 隐含绑定(Implicit Binding)

另一种要考虑的规则是:调用点是否有一个环境对象(context object),也称为拥有者(owning)或容器(containing)对象。

  • 看调用点是否在一个对象内,函数是否作为一个属性被调用obj.foo()。此时this指向对象内的其他属性。
隐含丢失(Implicitly Lost)

就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是 undefined。

  • 当对象进行函数引用obj.foo,实际上是函数本身的引用foo。调用点在全局作用域/传递一个回调函数,则此时发生隐含丢失,绑定退回默认绑定。
3. 明确绑定(Explicit Binding)

绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问 call(…) 和 apply(…)

  • call(..)
    通过 foo.call(..) 使用 明确绑定 来调用 foo,允许我们强制函数的 this 指向 obj
硬绑定(Hard Binding)
function foo() {
	console.log( this.a );
}

var obj = {
	a: 2
};

var bar = function() {
	foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// `bar` 将 `foo` 的 `this` 硬绑定到 `obj`
// 所以它不可以被覆盖
bar.call( window ); // 2
function foo(something) {
	console.log( this.a, something );
	return this.a + something;
}

var obj = {
	a: 2
};

var bar = foo.bind( obj );

var b = bar( 3 ); // 2 3
console.log( b ); // 5
4. new 绑定(new Binding)
function foo(a) {
	this.a = a;//这里的this即为下面构建的新对象bar,bar的a属性的值为foo的参数a的值
}

var bar = new foo( 2 );
console.log( bar.a ); // 2

通过在前面使用 new 来调用 foo(..),我们构建了一个新的对象并把这个新对象作为 foo(..) 调用的 thisnew 是函数调用可以绑定 this 的最后一种方式,我们称之为 new 绑定(new binding)。

5. 绑定优先级顺序

你需要做的 一切 就是找到调用点然后考察哪一种规则适用于它

  • 明确绑定 的优先权要高于 隐含绑定
function foo() {
	console.log( this.a );
}

var obj1 = {
	a: 2,
	foo: foo
};

var obj2 = {
	a: 3,
	foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
  • new 绑定 的优先级要高于 隐含绑定
function foo(something) {
	this.a = something;
}

var obj1 = {
	foo: foo
};

var obj2 = {};

obj1.foo( 2 );
console.log( obj1.a ); // 2

obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3

var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
  • new绑定 可以覆盖 硬绑定
function foo(something) {
	this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
6.判定 this

现在,我们可以按照优先顺序来总结一下从函数调用的调用点来判定 this 的规则了。按照这个顺序来问问题,然后在第一个规则适用的地方停下。

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。
var bar = new foo()
  1. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。
var bar = foo.call( obj2 )
  1. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。
var bar = obj1.foo()
  1. 否则,使用默认的 this(默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。
var bar = foo()
7.特例:被忽略的 this

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。

function foo(a,b) {
	console.log( "a:" + a + ", b:" + b );
}

// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 或者用ES6扩散操作符
foo(...[2, 3]);

// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
8.ES6: 箭头函数的词法 this

与使用四种标准的 this 规则不同的是,箭头函数从封闭它的(函数或全局)作用域采用 this 绑定。
是在函数定义的时候绑定。

function foo() {
  // 返回一个箭头函数
	return (a) => {
    // 这里的 `this` 是词法上从 `foo()` 采用的
		console.log( this.a );
	};
}
// 等价于:
function foo0() {
	var self = this; // 词法上捕获 `this`
	setTimeout( function(){
		console.log( self.a );
	}, 100 );
}

var obj1 = {
	a: 2
};

var obj2 = {
	a: 3
};

foo0.call(obj1);// 2

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是3!

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this且继承后不可以被覆盖
foo() 中创建的箭头函数在词法上捕获 foo() 被调用时的 this,不管它是什么。因为 foo()this 绑定到 obj1bar(被返回的箭头函数的一个引用)也将会被 this 绑定到 obj1。一个箭头函数的词法绑定是不能被覆盖的(就连 new 也不行!)。

一些题目
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()//作为对象方法进行引用调用,this指向person1,输出person1
person1.show1.call(person2)//改变this指向,输出person2

person1.show2()//箭头函数,捕获其上一级函数上下文中的this,即为window,输出window
person1.show2.call(person2)//call不能改变其绑定,仍然输出window

person1.show3()()//函数中返回函数,相当于函数调用,this指向window,输出window
person1.show3().call(person2)//改变了返回函数的this指向,输出person2
person1.show3.call(person2)()//改变show3对应函数的指向,但仍然是函数调用,this指向window,输出window

person1.show4()()//函数中返回箭头函数,捕获其上一级函数上下文的this,指向obj1,输出person1
person1.show4().call(person2)//不能改变箭头函数this指向,输出person1
person1.show4.call(person2)()//改变箭头函数上一级函数上下文的this指向,则此时this指向person2,输出person2

12.ES6、7、8新特性

ES6常用新特性总结

ES6解决了什么问题

ES6

面试:JavaScript基础概念_第12张图片
箭头函数、解构赋值、let、const、async / await
使用反引号 来创建字符串,可以包含由美元符号加花括号包裹的变量${vraible}。
参数默认值,不定参数,拓展参数[…arry]

类的支持(class关键字)

对象的创建,继承更加直观了,并且父类方法的调用,实例化,静态方法和构造函数等概念都更加形象化。

//类的定义
class Animal {
	//ES6中新型构造器
    constructor(name) {
        this.name = name;
    }
    //实例方法
    sayName() {
        console.log('My name is '+this.name);
    }
}
//类的继承
class Programmer extends Animal {
    constructor(name) {
    	//直接调用父类构造器进行初始化
        super(name);
    }
    program() {
        console.log("I'm coding...");
    }
}
//测试我们的类
var animal=new Animal('dummy'),
wayou=new Programmer('wayou');
animal.sayName();//输出 ‘My name is dummy’
wayou.sayName();//输出 ‘My name is wayou’
wayou.program();//输出 ‘I'm coding...’
解构赋值基本用法

(精华)2020年6月29日 JavaScript高级篇 ES6(解构赋值)

ES7
  • 求幂运算符(**)
Math.pow(3, 2) === 3 ** 2    // 9
  • Array.prototype.includes()方法
[1, 2, 3].indexOf(3) > -1 // true
等同于:
[1, 2, 3].includes(3) // true
  • 函数作用域中严格模式的变更。
ES8

async、await异步解决方案

Object.entries()、values()

字符串填充padStart()、padEnd()

ES9
  • for await of
    for of方法能够遍历具有 Symbol.iterator 接口的同步迭代器数据,但是不能遍历异步迭代器。ES9 新增的 for await of 可以用来遍历具有 Symbol.asyncIterator 方法的数据结构,也就是异步迭代器,且会等待前一个成员的状态改变后才会遍历到下一个成员,相当于 async 函数内部的 await。

  • Promise.prototype.finally()
    方法返回一个 Promise,在 promise 执行结束时,无论结果是 fulfilled 或者是 rejected,在执行 then() 和 catch() 后,都会执行 finally 指定的回调函数。

  • Object Resr & Spread 扩展运算符
    Object Rest SpreadES6 中添加的最意思的特性之一是 spread 操作符。你不仅可以用它替换 cancat() 和 slice() 方法,使数组的操作 (复制、合并) 更加简单,还可以在数组必须以拆解的方式作为函数参数的情况下,spread 操作符也很实用。

13.事件委托(代理)/ 事件冒泡 / 事件捕获

JavaScript事件代理(事件委托)

场景题:一个父元素中不断有div被加入,如何给这些div绑定事件。

  • 可以大量节省内存占用,减少事件注册
    如果给每个li列表项都绑定一个函数,那对内存的消耗是非常大的,因此较好的解决办法就是将li元素的点击事件绑定到它的父元素ul身上,执行事件的时候再去匹配判断目标元素

  • 可以实现当新增子对象时无需再次对其绑定(动态绑定事件)
    假设上述的例子中列表项li就几个,我们给每个列表项都绑定了事件;
    在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者删除列表项li元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;
    如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的;所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的

通过event.targettarget.id去定位元素

document.addEventListener("click", function (event) {
  var target = event.target;
  switch (target.id) {
    case "doSomething":
      document.title = "事件委托";
      break;
    case "goSomewhere":
      location.href = "http://www.baidu.com";
      break;
    case "sayHi": alert("hi");
      break;
  }
})

优点:

  • 节省内存占⽤,减少事件注册
  • 新增⼦对象时⽆需再次对其绑定事件,适合动态添加元素

局限性:

  • focus、blur 之类的事件本身没有事件冒泡机制,所以⽆法委托
  • mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗⾼,不适合事件委托
事件代理(委托)

事件代理(Event Delegation),又称之为事件委托。是JavaScript中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件(click、keydown…)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡

事件冒泡

当一个元素接收到事件的时候,会把他接收到的事件传给父级,一直传到window。
一个事件触发后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:
面试:JavaScript基础概念_第13张图片

  • 捕获阶段:从window对象传导到目标节点(上层传到底层)称为“捕获阶段”(capture phase),捕获阶段不会响应任何事件;
  • 目标阶段:在目标节点上触发,称为“目标阶段”
  • 冒泡阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层;

首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

阻止冒泡有如下方式:

  • 标准的W3C 方式:e.stopPropagation();这里的stopPropagation是标准的事件对象的一个方法,调用即可;不让事件向documen上蔓延,但是默认事件任然会执行,当你掉用这个方法的时候,如果点击一个连接,这个连接仍然会被打开

  • 非标准的IE方式:ev.cancelBubble=true; 这里的cancelBubble是 IE事件对象的属性,设为true就可以了

  • return false: 事件处理过程中,阻止了事件冒泡,也阻止了默认行为;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()和event.preventDefault()

  • event.preventDefault():事件处理过程中,不阻止事件冒泡,但阻止默认行为;调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;

window.onload=function(){
	document.getElementById("btn").addEventListener("click",function () {
	// body...
	alert("hello");
});
document.getElementById("div1").addEventListener("click",function(){
	alert("div1");
});
document.getElementById("div2").addEventListener("click",function(){
	alert("div2");
});
}

冒泡:从里面往外面触发事件,就是alert的顺序是 button、div2、div1。

捕获:从外面往里面触发事件,就是alert的顺序是div1、div2、button。

冒泡false捕获true:

要想冒泡,就要将每个监听事件的第三个参数设置为false,也就是默认的值。

要想捕获,就要将每个监听事件的第三个参数设置为true。

14.DOM事件模型

DOM之事件模型分脚本模型、内联模型(同类⼀个,后者覆盖)、动态绑定(同类多个)

<body>

<button onclick="javascrpt:alert('Hello')">Hello1button>

<button onclick="showHello()">Hello2button>

<button id="btn3">Hello3button>
body>

JavaScript事件模型主要分为3种:原始事件模型、DOM2事件模型、IE事件模型。

1.原始事件模型(DOM0级)

同⼀个元素,同类事件只能添加⼀个,如果添加多个,后⾯添加的会覆盖之前添加的

这是一种被所有浏览器都支持的事件模型,对于原始事件而言,没有事件流,事件一旦发生将马上进行处理,有两种方式可以实现原始事件:

(1)在html代码中直接指定属性值:

<button id="demo" type="button" onclick="doSomeTing()" />

(2)在js代码中为

document.getElementsById("demo").onclick = doSomeTing()

优点:所有浏览器都兼容
缺点:1)逻辑与显示没有分离;2)相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的,如:a.onclick = func1; a.onclick = func2;将只会执行func2中的内容。3)无法通过事件的冒泡、委托等机制(后面会讲到)完成更多事情。

因为这些缺点,虽然原始事件类型兼容所有浏览器,但仍不推荐使用。

2.DOM2事件模型

可以给同⼀个元素添加多个同类事件。

这种事件模型是捕获和冒泡模型。 此模型是W3C制定的标准模型,现代浏览器(IE6~8除外)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:
(1).事件捕获阶段,(2).事件目标阶段,(3).事件冒泡阶段。
在DOM2级中使用addEventListenerremoveEventListener来注册和解除事件(IE8及之前版本不支持)。这种函数较之之前的方法好处是一个dom对象可以注册多个相同类型的事件,不会发生事件的覆盖,会依次的执行各个事件函数。

addEventListener('事件名称','事件回调','捕获/冒泡')
3.在当前的事件模型中,哪些事件可以冒泡,哪些不会冒泡,为什么?不会冒泡的怎么实现事件委托

梳理下常见的不冒泡事件
(addEventListener第三个参数可以在捕获阶段添加事件监听)

15.异步编程/解决方案

js在执行过程中,每遇到一个异步函数,都会将这个异步函数放入一个异步队列中,只有当同步线程执行结束之后,才会开始执行异步队列中的函数。

前端异步(async)解决方案(所有方案)

1.async/await 对它有什么认识,还有它的问题?

异步操作同步写法、底层 generator、更符合线性思维更好读、可以 try catch 获取错误
问题:async / await 的传染性; await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

async:

  • 目的:为了解决大量复杂不易读的Promise异步的问题
  • async必须声明的是一个function;
  • await必须是在这个async声明的函数内部使用,否则就会报错;
  • async声明的函数的返回本质上是一个Promise

await:

  • await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖(async await本身就是promise + generator的语法糖。)
  • await是在等待一个Promise的异步返回
  • async 中代码是直接执行的(同步任务)
  • await是直接执行的,而await后面的代码是 microtask微任务
  • await后面代码会等await内部代码全部完成后再执行

适用场景:

  1. 一个请求接着一个请求,后一个请求依赖前一个请求
  2. 并发请求,有的时候我们并不需要等待一个请求回来才发出另一个请求,这样效率是很低的,所以这个时候就需要并发执行请求任务。(await + Promise.all)
  3. 错误处理:async/await 处理错误非常直观, 使用 try/catch 直接捕获就可以
  4. 超时处理:一个请求发出,我们是无法确定什么时候返回的,也总不能一直傻傻的等,设置超时处理有时是很有必要的(await + Promise.race )
2.ES6 Generator 函数

一次搞懂 Generator 函数

Generator 最大的特点就是可以控制函数的执行。

1、什么是 Generator 函数

Generator函数是ES6提供的一种异步编程解决方案,形式上也是一个普通函数,但有几个显著的特征:

– function关键字与函数名之间有一个星号 “*” (推荐紧挨着function关键字)
– 函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield)
– 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)
– 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态

Generator函数则没有执行而是返回一个Iterator对象,并通过调用Iterator对象的next方法来遍历,函数体内的执行看起来更像是“被人踢一脚才动一下”的感觉

每次调用Iterator对象的next方法时,内部的指针就会从函数的头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或return语句暂停。换句话说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next方法可以恢复执行

function* gen(){
    let res=yield 1
    yield res
    return 3
}
let g=gen();
console.log(g.next())//{value:1,done:false}
console.log(g.next(333))//{value:333,done:false}

在next中传入参数会作为上一次yield的返回值(会忽略第一个next中传递的参数)

3.async/defer标签

script没有async/defer标签会阻塞DOM渲染。

都只能对外部文件生效,内联文件不生效。

  1. async:HTML解析时,遇到了async属性的script脚本,则会异步下载和执行script脚本,同时浏览器继续进行HTML解析,如果有多个文件,不保证执行顺序
  2. defer:HTML解析时,遇到了defer属性的script脚本,则会新开一个线程下载script脚本,同时浏览器继续进行HTML解析,但script只会等到浏览器解析完HTML后才会执行,如果有多个文件,按照文件顺序执行
4.回调函数

在异步函数中将一个函数进行参数传入,当异步执行完成之后执行该函数。
如果再多几个异步函数,代码整体的维护性,可读性都变的极差,如果出了bug,修复的排查过程也变的极为困难,这个便是所谓的 回调函数地狱

发布/订阅者模式

订阅发布模式定义了一种一对多的依赖关系让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。vue就是基于发布/订阅者模式。

//创建一个主题发布类
var Publisher=function(){
    this.subscribers=[]
}


Publisher.prototype.publish=function(data){
    this.subscribers.forEach(function(fn){
        fn(data)
    })
}

/*
    在Function上挂载这个些方法,所有的函数都可以调用这些方法
    表示所有函数都可以订阅/取消订阅相关的主题发布
*/

//订阅
Function.prototype.subscribe=function(publisher){
    var that=this;
    var isExist=publisher.subscribers.some(function(el){
        if(el===that){
            return true
        }
    })
    if(!isExist){
        publisher.subscribers.push(that)
    }
    //return this是为了支持链式调用
    return this
}

//取消订阅
Function.prototype.unsubscribe=function(publisher){
    var that=this;

    //就是将函数从发布者的订阅者列表中进行删除
    publisher.subscribers=publisher.subscribers.filter(function(el){
        if(el!==that){
            return true
        }
    })

    return this
}

var publisher=new Publisher();
var subscriberObj=function(data){
    console.log(data)
}
subscriberObj.subscribe(publisher)

这样就实现了一个简单的发布订阅者模式,每次发布者发布新内容时,就会调用publish方法,然后将内容作为参数,依次调用订阅者函数(subscribers)。
其实,发布/订阅模式与事件监听很类似,

  • 事件监听是将一个回调函数事件绑定在一起,触发了相应事件,就会执行相应的回调函数
  • 发布/订阅模式是将订阅函数放入了发布者的订阅者列表中,更新时,遍历订阅者列表,执行所有的订阅者函数

16.Promise

ECMAscript 6 原生提供了 Promise 对象。

Promise 对象表示异步操作的最终完成(或失败)及其结果值

面试:JavaScript基础概念_第14张图片

Promise 对象有以下两个特点:
1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
pending: 初始状态,不是成功或失败状态。
fulfilled/resolved: 意味着操作成功完成。
rejected: 意味着操作失败。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。

创建 promise 时,PromiseStatus 将处于 pending 状态,并且 PromiseValue 为 undefined,直到 promise 被 resolvedrejected 为止。 当 promise 处于 resolvedrejected 的状态时,就称为 settled(已定型)。 所以 promise 通常从 pending 态转换到 settled 状态。

理解 Javascript 中的 Promise
使用 Promise

问题:

  1. Promise 不能被 try catch 捕获错误,因为 try catch 只能获取同步错误;如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
  2. Promise 的 then 链很长的时候,可读性很差
  3. Promise 当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
错误捕获
  1. then(resolve,reject); then方法中第二个回调,是失败时候做的失败时候做的事
function f(val){
    return new Promise((resolve,reject) => {
        if(val){
            resolve({ name:'小明' },100);  //成功时也可以传递一个值,但需要注意的是只能传递一个参数,传两个的话第二个参数是拿不到的
        }else{
            reject('404');  //错误处理的第一种方式:传递参数,错误的原因
        }
    });
}
         
//then(resolve,reject),碰到resolve或是reject都是异步的,所以tr catch对它是没有用的
//then方法中的第二个回调reject,是失败时候做的事
f(false)
   .then( (data, data2) => {
       console.log(data2);  //undefined
   }, e => {  
       console.log(e);  //404
   })
  1. 使用catch捕获错误
function f(val){
   return new Promise((resolve,reject) => {
       if(val){
           resolve({ name:'小明' });  
       }else{
           reject('404');  
       }
   });
}

f(true)
   .then(data => {  
       console.log(data); //{name:'小明'}
       return f(false);  //返回的promise是失败的话,后面的then对这个失败没有处理的话,就会继续往下走
   })
   .then(() => {
       console.log('我永远不会被输出')
   })
   .then(() => {
   }, e => console.log('失败')) //
   .catch(e => {  //上面处理了错误的话,这个catch就不会运行了
       console.log(e);  //404
   })
   .then(() => {  //catch后面可以继续then,但是如果后面的then出错了,跟上一个catch就没有关系了
       console.log(e)
       return f(false)
   })
   .catch()  //如果最后一个catch有错误,会无限catch
   //标准es中,这个问题没有很好地解决方法,但是第三方的库有对全局的捕获
  1. finally捕获
//finally
//不论成功还是失败,finally中的内容一定会执行
//可以在finally中做一些收尾的工作
function f(val){
    return new Promise((resolve,reject) => {
        if(val){
            resolve({ name:'小明' });  
        }else{
            reject('404');  
        }
    });
}

f(true)
    .then(data => {
        console.log(data);  //{name:'小明'}
        return f(false);
    })
    .catch(e => {
        console.log(e)   //404
        return f(false);  //即便返回了一个成功的promise,下面的finally也会执行,如果返回的是失败的promise,控制台最后一行会报错uncaught (in promise) 404
    })
    .finally( () => {
        console.log(100)  //100
    })

17.ES6:let var const区别

  1. let 声明的变量只在 let 命令所在的代码块 (块级作用域)内有效。var 是在 全局范围内有效。
{
  let a = 0;
  var b = 1;
}
a  // ReferenceError: a is not defined
b  // 1
  1. const 声明一个只读的常量,一旦声明,常量的值就不能改变。
  • 对基本类型而言
    对于基本的类型而言的话,比如number,string,boolean等来说,确实它就是声明一个不会变的常量,只要你修改了它,就会报错
const a = 1
a = 2 // Uncaught TypeError: Assignment to constant variable.
const b = '1231'
b = 'xcv' // Uncaught TypeError: Assignment to constant variable.
const c = true
c = false // Uncaught TypeError: Assignment to constant variable.
  • 对引用类型而言
    对于引用类型而言的话,它指的并不会对象的内容不变,而是对象的地址不变。也就是说,你可以修改对象的内部成员,但是你不可以修改该变量的地址。
  const obj = {
    name: 'cjg'
  }
  obj.school = 'sysu'
  console.log(obj) // Object {name: "cjg", school: "sysu"}
  obj = {} // VM183:6 Uncaught TypeError: Assignment to constant variable
  1. let 只能声明一次 var 可以声明多次
let a = 1;
let a = 2;
var b = 3;
var b = 4;
a  // Identifier 'a' has already been declared
b  // 4
for (var i = 0; i < 10; i++) {
  setTimeout(function(){
    console.log(i);
  })
}
// 输出十个 10
for (let j = 0; j < 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}
// 输出 0123456789

变量 i 是用 var 声明的,在全局范围内有效,所以全局中只有一个变量 i, 每次循环时,setTimeout 定时器里面的 i 指的是全局变量 i ,而循环里的十个 setTimeout 是在循环结束后才执行,所以此时的 i 都是 10。

变量 j 是用 let 声明的,当前的 j 只在本轮循环中有效,每次循环的 j 其实都是一个新的变量,所以 setTimeout 定时器里面的 j 其实是不同的变量,即最后输出 12345。(若每次循环的变量 j 都是重新声明的,如何知道前一个循环的值?这是因为 JavaScript 引擎内部会记住前一个循环的值)。

  1. let 不存在变量提升,var 会变量提升
console.log(a);  //ReferenceError: a is not defined
let a = "apple";
 
console.log(b);  //undefined
var b = "banana";

变量 b 用 var 声明存在变量提升,所以当脚本开始运行的时候,b 已经存在了,但是还没有赋值,所以会输出 undefined。

变量 a 用 let 声明不存在变量提升,在声明变量 a 之前,a 不存在,所以会报错。

  1. const 声明一个只读变量,声明之后不允许改变。意味着,一旦声明必须初始化,否则会报错
const PI = "3.1415926";
PI  // 3.1415926

const MY_AGE;  // SyntaxError: Missing initializer in const declaration    
  1. let const定义的变量由于作用域,并不会绑定到window上
    ES6规定,var命令与function命令声明的全局变量,依旧是顶层对象的属性,但let命令,const命令、 class命令声明的全局变量,不属于顶层的属性。
  • ES5声明变量只有两种方式:var和function。
  • ES6有let、const、import、class再加上ES5的var、function共有六种声明变量的方式。
  • 还需要了解顶层对象:浏览器环境中顶层对象是window,Node中是global对象。
  • ES5中,顶层对象的属性等价于全局变量。(敲黑板了啊)
  • ES6中,有所改变:var、function声明的全局变量,依然是顶层对象的属性;let、const、class声明的全局变量不属于顶层对象的属性,也就是说ES6开始,全局变量和顶层对象的属性开始分离、脱钩。

所以ES6非严格模式下,与var声明的全局变量都会成为window的属性
而使用let声明的全局变量,不会成为window的属性

暂时性死区
var PI = "a";
if(true){
  console.log(PI);  // ReferenceError: PI is not defined
  const PI = "3.1415926";
}

ES6 明确规定,代码块内如果存在 let 或者 const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。代码块内,在声明变量 PI 之前使用它会报错。

注意要点

const 如何做到变量在声明初始化之后不允许改变的?其实 const 其实保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动。此时,你可能已经想到,简单类型和复合类型保存值的方式是不同的。是的,对于简单类型(数值 number、字符串 string 、布尔值 boolean),值就保存在变量指向的那个内存地址,因此 const 声明的简单类型变量等同于常量。而复杂类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了,所以使用 const 声明复杂类型对象时要慎重。

为什么let和const不能重复声明

在ES6规范有一个词叫做Global Enviroment Records(也就是全局环境变量记录),它里面包含两个内容,一个是Object Enviroment Record,另一个是Declarative Enviroment Record。函数声明和使用var声明的变量会添加进入Object Enviroment Record中,而使用let声明和使用const声明的变量会添加入Declarative Enviroment Record中。

  • 使用var声明时,V8引擎只会检查Declarative Enviroment Record中是否有该变量,如果有,就会报错,否则将该变量添加入Object Enviroment Record中。
  • 使用let和const声明时,引擎会同时检查Object Enviroment Record和Declarative Enviroment Record,如果有,则报错,否则将将变量添加入Declarative Enviroment Record中。

18.箭头函数和普通函数区别

面试:JavaScript基础概念_第15张图片

  1. 箭头函数没有原型属性/construct方法,因此不能通过new方法调用创建实例
let Arrow = () => {
	console.log(arguments);
}

console.log(Arrow.prototype);  // undefined
let newArrow = new Arrow();
//Uncaught TypeError: Arrow is not a constructor 
  1. 箭头函数没有arguments,但可以用rest参数…解决
let Arrow1 = () => {
	console.log(arguments);
}
Arrow1(1, 2, 3);
//VM465:2 Uncaught ReferenceError: arguments is not defined 

let Arrow2 = (...args) => {
	console.log(args);
}

Arrow2(1, 2, 3);
  1. 箭头函数不绑定this,只在其定义时捕获上下文的this值,且之后不可以被call/apply/bind改变
let obj2 = {
    a: 10,
    b: function(n) {
        let f = (n) => n + this.a;//this指向obj
        return f(n);
    },
    c: function(n) {
        let f = (n) => n + this.a;
        let m = {
            a: 20
        };
        return f.call(m,n);//this仍然指向obj
    },
    d: function(n) {
    	console.log(this.a)
    }
};
var a = 5;
console.log(obj2.b(1));  // 11
console.log(obj2.c(1)); // 11
console.log(obj2.d()); // 10
console.log(obj2.d.call(this, a)); // 5

5.箭头函数不能当做Generator函数,不能使用yield关键字

使用场景
  1. 箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如:用在 map、reduce、filter 的回调函数定义中
  2. 箭头函数的亮点是简洁,但在有多层函数嵌套的情况下,箭头函数反而影响了函数的作用范围的识别度,这种情况不建议使用箭头函数
  3. 箭头函数要实现类似纯函数的效果,必须剔除外部状态。所以箭头函数不具备普通函数里常见的 this、arguments 等,当然也就不能用 call()、apply()、bind() 去改变 this 的指向
  4. 箭头函数不适合定义对象的方法(对象字面量方法、对象原型方法、构造器方法),因为箭头函数没有自己的 this,其内部的 this 指向的是外层作用域的 this
  5. 箭头函数不适合定义结合动态上下文的回调函数(事件绑定函数),因为箭头函数在声明的时候会绑定静态上下文

19.作用域:词法作用域和动态作用域

调用函数:RHS引用
函数内将值作为参数传给函数时:LHS查询

作用域:是通过标识符名称查询变量的一组规则/收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施一组严格的规则。
这种查询也许是为了向这个变量赋值,这时变量是一个 LHS(左手边)引用,或者是为取得它的值,这时变量是一个 RHS(右手边)引用

遍历嵌套 作用域 的简单规则:引擎 从当前执行的 作用域 开始,在那里查找变量,如果没有找到,就向上走一级继续查找,如此类推。如果到了最外层的全局作用域,那么查找就会停止,无论它是否找到了变量。

如果 RHS 查询在嵌套的 作用域 的任何地方都找不到一个值,这会导致 引擎 抛出一个 ReferenceError

现在,如果一个 RHS 查询的变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError

ReferenceError 是关于 作用域 解析失败的,而 TypeError 暗示着 作用域 解析成功了,但是试图对这个结果进行了一个非法/不可能的动作。

词法作用域:JavaScript 所采用的作用域模型

JavaScript 采用是词法作用域(lexical scoping),也就是 静态作用域

  • 函数的作用域在函数定义的时候就决定了

作用域:(scope)是标识符(变量)在程序中的可见性范围,它关注的是标识符(变量)的可访问性(可见性)
程序设计概念:通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的 作用域

定义:在词法分析时被定义的作用域。它是一组关于 引擎 如何查询变量和它在何处能够找到变量的规则。词法作用域是基于,你,在写程序时,变量和作用域的块儿在何处被编写决定的,因此它在词法分析器处理你的代码时(基本上)是固定不变的。

一旦找到第一个匹配,作用域查询就停止了相同的标识符名称可以在嵌套作用域的多个层中被指定,这称为“ 遮蔽(shadowing) ”(内部的标识符“遮蔽”了外部的标识符)。无论如何遮蔽,作用域查询总是从当前被执行的最内侧的作用域开始,向外/向上不断查找,直到第一个匹配才停止。

不管函数是从 哪里 被调用的,也不论它是 如何 被调用的,它的词法作用域是由 这个函数被声明的位置 唯一 定义的。

动态作用域:JavaScript没有动态作用域

与词法作用域对应的还有一个 动态作用域

  • 函数的作用域是在函数调用的时候才决定的

动态作用域看起来在暗示,有充分的理由,存在这样一种模型,它的作用域是在运行时被确定的,而不是在编写时静态地确定的。

function foo() {
	console.log( a ); // 2
}

function bar() {
	var a = 3;
	foo();
}

var a = 2;

bar();

动态作用域本身不关心函数和作用域是在哪里和如何被声明的,而是关心 它们是从何处被调用的。换句话说,它的作用域链条是基于调用栈的,而不是代码中作用域的嵌套。

foo() 不能为 a 解析出一个变量引用时,它不会沿着嵌套的(词法)作用域链向上走一层,而是沿着调用栈向上走,以找到 foo()从何处 被调用的。如果JS有动态作用域,那么因为 foo() 是从 bar() 中被调用的,它就会在 bar() 的作用域中检查变量,并且在这里找到持有值 3 的 a。

但是 this 机制有些像动态作用域,this 关心的是 函数是如何被调用的。

关键的差异:词法作用域是编写时的,而动态作用域(和 this)是运行时的。词法作用域关心的是 函数在何处被声明,但是动态作用域关心的是函数 从何处 被调用

作用域链(Scope Chain)

根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。

作用域链的作用主要用于查找标识符,当作用域需要查询变量的时候会沿着作用域链依次查找,如果找到标识符就会停止搜索,否则将会沿着作用域链依次向后查找,直到作用域链的结尾。

执行环境(execution context)

每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?

js为每一个执行环境(EC)关联了一个变量对象(VO)。环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个 执行环境栈(ECS) 中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。

function f1() {
     var f1Context = 'f1 context';
     function f2() {
         var f2Context = 'f2 context';
         function f3() {
             var f3Context = 'f3 context';
             console.log(f3Context);
         }
         f3();
         console.log(f2Context);
     }
     f2();
     console.log(f1Context);
 }
 f1();

面试:JavaScript基础概念_第16张图片

  1. 全局上下文压入栈顶
  2. 执行某一函数就为其创建一个EC,并压入栈顶
  3. 栈顶的函数执行完之后它的EC就会从ECS中弹出,并且变量对象(VO)随之销毁
  4. 所有函数执行完之后ECS中只剩下全局上下文,在应用关闭时销毁

其他

1.JS执行

JavaScript代码是如何被执行的
JavaScript是单线程语言。所以JavaScript是按顺序执行的:先编译再执行,但其中存在变量提升

主要核心流程分为两步 – 编译执行

  1. 首先将 JavaScript代码 转换为 低级中间代码 或者 机器能够理解的机器代码 ;
  2. 执行转换后的代码并输出执行结果;

面试:JavaScript基础概念_第17张图片

  1. 执行之前,准备所需的基础环境
    在 V8 启动执行 JavaScript 之前,它还需要准备执行 JavaScript 时所需要的一些基础环境,这些基础环境包括了 堆空间、栈空间、全局执行上下文、全局作用域、消息循环系统、内置函数 等,这些内容都是在执行 JavaScript 过程中需要使用到的。

比如:

  • JavaScript 全局执行上下文就包含了执行过程中的全局信息,比如一些内置函数,全局变量等信息;
  • 全局作用域包含了一些全局变量,在执行过程中的数据都需要存放在 内存 中;
  • 由于 V8 采用了经典的堆和栈的管理内存管理模式,所以 V8 还需要初始化了内存中的堆和栈结构;
  • 另外,要我们的 V8 系统活起来,还需要初始化消息循环系统 ,消息循环系统包含了 消息驱动器消息队列 ,它如同 V8 的心脏,不断接受消息并决策如何处理消息。
  1. 准备好基础环境,向V8提交要执行的JS代码
    首先,V8 会接收到要执行的 JavaScript 源代码,不过这对 V8 来说只是一堆 字符串,V8 并不能直接理解这段字符串的含义,它需要 结构化 这段字符串;

  2. 结构化字符串(JS源代码)
    结构化,是指信息经过分析后可 分解成多个互相关联的组成部分。各组成部分间有明确的层次结构,方便使用和维护,并有一定的操作规范;

  3. 生成AST、作用域
    结构化之后,就生成了 抽象语法树(AST),AST 是便于 V8 理解的结构;在生成 AST 的同时,V8 还会 生成相关的作用域,作用域中存放相关变量;

  4. 生成字节码
    生成AST的步骤可以拆分成以下两个小步骤:

  • 词法分析:将JavaScript代码解析成一个个词法单元(token)
  • 语法分析:将词法单元根据一定规则组装成抽象语法树
    有了 AST 和 作用域 之后,接下来就可以生成 字节码 了,字节码是介于 AST 和 机器代码 的中间代码。但是与特定类型的机器代码无关,解释器可以直接解释执行字节码 ,或者通过编译器将其编译为二进制的机器代码再执行;
  1. 解释器解释执行字节码
    生成字节码之后,解释器就登场了,它会 按照顺序 解释执行字节码,并输出执行结果

面试:JavaScript基础概念_第18张图片

2.Symbol / BigInt

Symbol

ES5的对象属性名都是字符串,这容易造成命名冲突。ES6引入一种新的数据类型:Symbol,用于创建独一无二的值。Symbol 通过 Symbol() 函数生成

基本用法

symbol不是一个构造函数,不能使用new来创建,与普通函数调用类似

var s1 = Symbol();//创建了一个symbol的实例
console.log(s1);//Symbol()
console.log(typeof s1);//symbol

symbol可以传递一个字符串参数,参数作用是对symbol类型的描述,便于区分这个symbol是哪一个

var s1 = Symbol("symbol");
console.log(s1);//Symbol(symbol)
console.log(typeof s1);//symbol

symbol类型的值具有唯一性,是一个独一无二的值,每一个 Symbol 的值都不相等。相同参数 Symbol() 返回的值不相等

var s1 = Symbol("symbol");
var s2 = Symbol("symbol");
console.dir(s1 == s2);//false

Symbol作为属性名,该属性不会出现在 for in, for of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames() 返回。但他也不是私有属性,有一个Object.getOwnPropertySymbols() (和Reflect.ownKeys())方法可以获取对象的所有 Symbol 属性。

  • Symbol.for()

Symbol.for()方法首先会搜索’全局Symbol注册表’,看是否存在一个键值为 “uid” 的’Symbol’。如果存在,该方法会’返回这个已存在的Symbol’,否则,会创建一个新的’Symbol’,并使用该键值将其记录到全局’Symbol’注册表中,然后’返回这个新的Symbol’。这就意味着此后使用同一个键值去调用’Symbol.for()‘方法都将会’返回同一个Symbol’

// 创建一个全局的Symbol
let uid = Symbol.for("uid")

// 在对象中使用这个uid私有属性
let object = {
    [uid]: "12345"
}

console.log(object[uid])    // "12345"
console.log(uid)            // "Symbol(uid)"

// 查看全局Symbol注册表中是否有uid,如果有就使用现有的,如果没有就创建新的Symbol
let uid2 = Symbol.for("uid")

console.log(uid === uid2)   // true
console.log(object[uid2])   // "12345"
console.log(uid2)           // "Symbol(uid)"
  • Symbol.keyFor()

你可以使用’Symbol.keyFor()'方法在’全局Symbol注册表’中根据’Symbol’检索出对应的键值

let uid = Symbol.for("uid")
console.log(Symbol.keyFor(uid))     // "uid"

let uid2 = Symbol.for("uid")
console.log(Symbol.keyFor(uid2))    // "uid"

let uid3 = Symbol("uid")
console.log(Symbol.keyFor(uid3))    // undefined,因为全局Symbol注册表中没有uid3,uid3是局部的
  • 只能使用"[]"运算符

以 Symbol 值作为属性名,必须使用**[]**运算符定义和获取属性值

如果对象下同时添加了与symbol同名的常规属性,则使用[]访问的是symbol属性,使用点运算符访问的是常规属性

  • 无法被转换为字符串值或数值
var n = Symbol("uname");
var obj = {};
obj.n = "jerry";
obj[n] = "tom";
console.log(obj[n]);//tom
console.log(obj.n);//jerry
应用场景
  1. 应用场景1:使用Symbol来作为对象属性名(key)
  2. 应用场景2:使用Symbol来替代常量
  3. 应用场景3:使用Symbol定义类的私有属性/方法
    由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。
BigInt

JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。

但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。

BigInt 是一种内置对象,它提供了一种方法来表示大于2^53 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n

const hugeHex = BigInt("0x1fffffffffffff");
// ↪ 9007199254740991n

const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
// ↪ 9007199254740991n

使用 typeof 测试时, BigInt 对象返回 “bigint” :

typeof 1n === 'bigint'; // true
typeof BigInt('1') === 'bigint'; // true

2.数组的方法都有哪些

  • fill(value, start, end):将数组start到end的值用value填充
  • concat()
  • join() ,数组变字符串,默认用‘,’分隔,一般用arr.join(‘’)
  • push()
  • pop()
  • shift()
  • unshift()
  • slice() ,左闭右开
  • splice(index,howmany,item1,.....,itemX) 向/从数组中添加/删除项目,然后返回被删除的项目。该参数是开始插入和(或)删除的数组元素的下标,index包括自身
  • sort()
  • reverse()
  • indexOf()/lastIndexOf()(前往后/后往前)
  • every()/some() (每一项/任意一项)
  • filter() :返回数组内所有满足条件的元素
  • map()
  • forEach()
    ES6:
  • find() :查找第一个符合条件的数组元素
  • findIndex() :查找第一个符合条件的数组元素的索引
  • includes()

3.map和forEach区别

面试:JavaScript基础概念_第19张图片

相同点
  • 都是循环遍历数组中的每一项
  • forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项),index(索引值),arr(原数组)
  • 匿名函数中的this都是指向window
  • 只能遍历数组
  • 都不会改变原数组
不同点

map方法
1.map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
2.map方法不会对空数组进行检测,map方法不会改变原始数组。

forEach方法
1.forEach方法用来调用数组的每个元素,将元素传给回调函数。
2.forEach对于空数组是不会调用回调函数的。

4.Set/Map/WeakSet/WeakMap

深入理解ES6–7.Set和Map

1. Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

[value, value],键值与键名是一致的(或者说只有键值,没有键名);

可以遍历,keys(),values(),entries()

方法有:add、delete、has、clear。set.size返回大小

Array.from方法可以将Set结构转为数组。

2.WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  1. 只接受对象作为键名(null除外),成员都是对象;
  2. 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;

不能遍历,方法有 add、delete、has

3.Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

可以遍历keys()、values()、entries()、forEach(),方法有set、get、has、delete,可以跟各种数据格式转换。

4.WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

只接受对象最为键名(null 除外),不接受其他类型的值作为键名;

键名是弱引用,不会被计入垃圾回收机制,不阻止垃圾回收器回收它所引用的 key。键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;

WeakMap的专用场合就是,它的键所对应的对象(如dom节点),可能会在将来消失。WeakMap结构有助于防止内存泄漏。

不能遍历,方法有 get、set、has、delete。


set的方法:add、delete、has

map的方法:set、delete、has、get

set 无序且不重复

Map和WeakMap的区别:

WeakMap 的 key 只能是对象,且不能被遍历。当把 WeakMap 里面的 key 设为 null 之后,可以被垃圾回收机制回收,可以用于存储DOM节点。

let a = {name: "sillywa"}
let b = {age: 90}
let map = new Map()
let weakMap = new WeakMap()
map.set(a, "test1")
weakMap.set(b, "test2")
 
a=null
b=null   
map.get(a)
weakMap.get(b)

5.类型转换

让人头疼的类型转换

6.模块化:什么是amd、commonjs、umd、esm?

模块化是一个语言发展的必经之路,其能够帮助开发者拆分和组织代码,随着前端技术的发展,前端编写的代码量也越来越大,就需要对代码有很好的管理,而模块化能够帮助开发者解决命名冲突、管理依赖、提高代码的可读性、代码解耦以及提高代码的复用性

  • 语法差异
  1. CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。而ES6模块化在浏览器和node.js中都可以用。
  2. 语法上面,两者最明显的差异是,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export。
  3. 在node.js使用模块化,需要将 CommonJS 脚本的后缀名都改成.cjs,ES6 模块采用.mjs后缀文件名。或者修改package.json里面的文件,type字段为module或commonjs。
  • 在 Node.js 中使用模块化,ES6 模块与 CommonJS 模块差异
  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    注意:CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
    ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    原因:CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
  • AMD
    AMD异步模块定义,全称Asynchronous Module Definition规范,是浏览器端的模块化解决方案,CommonJS规范引入模块是同步加载的,这对服务端不是问题,因为其模块都存储在硬盘上,可以等待同步加载完成,但在浏览器中模块是通过网络加载的,若是同步阻塞等待模块加载完成,则可能会出现浏览器页面假死的情况,AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // do something
});

define(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // do something
    return {};
});
  • CMD
    CMD通用模块定义,是SeaJS在推广过程中对模块定义的规范化产出,也是浏览器端的模块化异步解决方案,CMD和AMD的区别主要在于:
    对于依赖的模块,AMD是提前执行(相对定义的回调函数, AMD加载器是提前将所有依赖加载并调用执行后再执行回调函数),CMD是延迟执行(相对定义的回调函数, CMD加载器是将所有依赖加载后执行回调函数,当执行到需要依赖模块的时候再执行调用加载的依赖项并返回到回调函数中),不过RequireJS从2.0开始,也改成可以延迟执行
    AMD是依赖前置(在定义模块的时候就要声明其依赖的模块),CMD是依赖就近(只有在用到某个模块的时候再去require——按需加载,即用即返)。
define(function(require,exports,module){
  var a = reuire('require.js');
  a.dosomething();
  return {};
});

7.for in/for of区别

  1. 遍历数组
  • for in遍历的是数组的索引(即键名),index索引为字符串型数字,不能直接进行几何运算;会遍历数组所有的可枚举属性,包括原型。
  • for of遍历的是数组元素值(即键值)。

for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name

  1. 遍历对象
  • 通常用for in来遍历对象的键名
  • for in 可以遍历到myObject的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部判断一下,hasOwnPropery方法可以判断某属性是否是该对象的实例属性

8.面向对象面向的三大基本特征,五大基本原则

  1. 封装
    封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别,将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。

封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员。

面相对象的不就是使用程序处理事情时以对象为中心去分析吗,与面向过程不同,面向过程关心处理的逻辑、流程等问题,而不关心事件主体。而面向对象即面向主体,所以我们在解决问题时应该先进行对象的封装(对象是封装类的实例,比如张三是人,人是一个封装类,张三只是对象中的一个实例、一个对象)。

  1. 继承
    继承是面向对象的基本特征之一,继承机制允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
    继承机制可以很好的描述一个类的生态,也提高了代码复用率.

  2. 多态
    同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。

多态存在的三个必要条件:

  • 继承
  • 重写(子类继承父类后对父类方法进行重新定义)
  • 父类引用指向子类对象

简言之,多态其实是在继承的基础上的

9.JSON.stringify()和JSON.parse()应用场景

  • JSON.stringify():将对象、数组转换成字符串
  • JSON.parse():将字符串转成json对象

1、
a、浏览器创建、获取(sessionStorage、localStorage)数组内容
b、路由(浏览器地址)传参、获取数组内容
创建、传参的时候使用JSON.stringify()
(如果不使用JSON.stringify()存进去的将是[object object],所以如果我们开发中遇到了获取内容的时候是[object object]不妨试试JSON.stringify()),
比如创建sessionStorage

sessionStorage.setItem("keyName",JSON.stringify(data))
  1. 实现对象深拷贝
function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj))
}
  1. 判断数组是否包含某对象,或者判断对象是否相等
//判断数组是否包含某对象
let data = [
    {name:'echo'},
    {name:'听风是风'},
    {name:'天子笑'},
    ],
    val = {name:'天子笑'};
JSON.stringify(data).indexOf(JSON.stringify(val)) !== -1;//true

//判断两数组/对象是否相等
let a = [1,2,3],
    b = [1,2,3];
JSON.stringify(a) === JSON.stringify(b);//true

10.JS编译过程

编译阶段编译阶段分为 词法分析、 语法分析、 预编译 三个阶段。

  1. 词法分析
    字符流 转换为 词法单元流(token),就像英文句子一个个单词独立翻译,举例:
    var result = testNum1 - testNum2;
    词法分析后的结果为:
    NAME “result”
    EQUALS
    NAME “testNum1”
    MINUS
    NAME “testNum2”
    SEMICOLON

  2. 语法分析
    将上一步生成的 token 数据根据语法规则生成对应的 “抽象语法树”(Abstract Syntax Tree, AST)。
    在这个过程中 JS 会对全部的脚本代码进行解析,通过 Javascript引擎 检查你的代码是否存在错误,如果有格式错误就会抛出一个错误。
    如果成功构建完 AST,接着就通过 AST 生成字节码,也就生成了计算机可执行的代码,该过程叫做代码生成。

    如:var a=2;
    抽象语法树转为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将值 2 储存在 a 中。

  3. 预编译(并非完全按顺序执行)
    当 JavaScript引擎 解析脚本时,他会在预编译阶段对所有声明的 变量 和 函数 进行处理。
    在这个过程中,会先预声明变量,之后再预声明函数(也就是变量提升优先于函数提升)。

    JavaScript引擎在预编译的过程中,碰到任何变量,如果变量未经声明就赋值,就会将此变量归为全局对象所有。

脚本代码块script执行前

  1. 查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined
  2. 查找函数声明,函数名作为全局对象的属性,值为函数引用

函数执行前:

  1. 创建AO对象(Active Object)
  2. 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined
  3. 实参形参相统一,实参值赋给形参
  4. 查找函数声明,函数名作为AO对象的属性,值为函数引用

11. sort()用的什么排序方式

火狐:归并
chrome:插入排序和快速排序结合的排序算法。数组长度不超过10时,使用插入排序。长度超过10使用快速排序。在数组较短时插入排序更有效率。

12.深拷贝/浅拷贝

面试:JavaScript基础概念_第20张图片

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

JSON.parse(JSON.stringify(obj))的问题
  1. 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
  2. 如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
  3. 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
  4. 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
  5. JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
  6. 如果对象中存在循环引用的情况也无法正确实现深拷贝;

关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑

13.requestAnimationframe

是浏览器提供的一个按帧对网页进行重绘的 API

requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:

  1. requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
  2. 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。

问题:requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

执行时间:
requestAnimationFrame是个宏任务,在浏览器每次刷新页面之前执行。它但它和那些平行级别的宏任务执行顺序是不确定的

14.forEach跳出循环

通过抛出异常的方式跳出循环 通过return跳过当次循环

var arr = [1,3,5,7,9];
var id = 5;
try {
    arr.forEach(function (curItem, i) {
        if(curItem === 1) return;
        console.log(curItem)
        if (curItem === id) {
            throw Error();         //满足条件,跳出循环
        }
    })
 } catch (e) {
 }

15.检测空对象

Object.entries(obj).length === 0 && obj.constructor === Object // ES7
Object.keys(obj).length === 0 && obj.constructor === Object // ES6

// ES5
function isEmpty(obj) {
    for(var prop in obj) {
      if(obj.hasOwnProperty(prop)) {
        return false;
      }
    }
    return JSON.stringify(obj) === JSON.stringify({});
  }

// loadash/underscore
_.isEmpty({}); // true

16.if-else/switch

  • switch case与if else的区别:
    switch case会生成一个跳转表来指示实际的case分支的地址,而if…else却需要遍历条件分支直到命中条件

  • switch case的优缺点
    (1)switch case的优点:
    当分支较多时,用switch的效率是很高的。因为switch是确定了选择值之后直接跳转到那个特定的分支.

(2)switch case的缺点:

  1. switch…case占用较多的代码空间,因为它要生成跳表,特别是当case常量分布范围很大但实际有效值又比较少的情况,switch…case的空间利用率将变得很低。
  2. switch…case只能处理case为常量的情况。
  • if else的优缺点
    (1)if else的优点:if else能应用于更多的场景,所以if else比较灵活。
    (2)if else的缺点:if else必须遍历所有的可能值。

17.数组/对象遍历方法

数组遍历
  1. for循环
for(j = 0; j < arr.length; j++) {
   
} 

// 将arr.length提出来性能更高
const lenght = arr.length;
for(j = 0; j < length; j++) {
   
} 
  1. foreach
  2. for in
  3. map
  4. for of

性能比较:for循环 > for of > foreach > map > for in

对象遍历
  1. for......in会得到对象原型链上的属性
  2. Object.keys(obj).forEach()/Object.values(obj).forEach()/Object.entries(obj)
  3. Object.getOwnPropertyNames(obj)
    返回一个数组,包含对象自身的所有属性(包含不可枚举属性)
    遍历可以获取key和value
const obj = {
        id:1,
        name:'zhangsan',
        age:18
}
Object.getOwnPropertyNames(obj).forEach(function(key){
    console.log(key+ '---'+obj[key])
})
  1. Reflect.ownKeys(obj)
    返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举(包括不可枚举的属性和 Symbol 属性)
var obj = {'0':'a','1':'b','2':'c'};
Reflect.ownKeys(obj).forEach(function(key){
	console.log(key,obj[key]);
});
  1. Object.getOwnPropertySymbols(obj)
    方法返回对象自身的 Symbol 属性组成的数组,不包括字符串属性
var obj = {'0':'a','1':'b','2':'c'};
Object.getOwnPropertySymbols(obj).forEach((key) => {
 console.log(obj[key])
}); // 什么都没有,因为该对象还没有 Symbol 属性

// 给对象添加一个不可枚举的 Symbol 属性
Object.defineProperties(obj, {
 [Symbol('baz')]: {
  value: 'Symbol baz',
  enumerable: false
 }
})

// 给对象添加一个可枚举的 Symbol 属性
obj[Symbol('foo')] = 'Symbol foo'

Object.getOwnPropertySymbols(obj).forEach((key) => {
 console.log(obj[key]) // Symbol baz, Symbol foo
})

面试:JavaScript基础概念_第21张图片

18.二进制/十进制互相转换

  • 十进制转换为二进制:NumberObject.toString(radix);,radix为进制数
var num = 100;
console.log(num.toString(2)); // 1100100

toString()方法可把一个 Number 对象转换为一个字符串,并返回结果。

  • 二进制转十进制:parseInt(string, radix);,radix为进制数
var num = '1100100';
console.log(parseInt(num,2)); // 

parseInt() 函数可解析一个字符串,并返回一个整数。

19.面向对象写js有什么优缺点

通过类似传统面向对象语言,使用构造函数方式 为每个实例添加方法和属性, 这种方式,存在一个问题, 不能达到函数共用,每个实例都会复制到方法。
一般,我们可以通过原型属性(prototype)改造这种方式,达到不同实例共用同一个方法,也可以把原型对象上的所有方法,使用字面量方式简写

如果我们想把面向对象的使用方式更加的优雅,比如链式调用, 我们应该在每个方法中返回对象本身,才能继续调用方法, 即返回this;

var Util = function(){
     return {
         checkUserName : function(){
             console.log( 'userName...' );
             return this;
         },
         checkUserPwd : function(){
             console.log( 'userPwd...' );
             return this;
         },
         checkUserEmail : function(){
             console.log( 'userEmail...' );
             return this;
         }
     }
 }
 // 方法中如果没有返回this,下面这种调用方式是错误的
 Util().checkUserEmail().checkUserName();

 // 方法中返回对象本身,可以链式调用
 Util().checkUserEmail().checkUserName().checkUserPwd();

在实际开发中,我们经常需要扩展一些功能和模块。扩展可以在本对象或者父类对象或者原型上,从功能上来说,是没有问题的,但是确造成了全局污染:通俗点说,并不是说有的函数都需要checkUserName这个方法,而我们这样写,所有的函数在创建过程中都会从父类的原型链上继承checkUserName, 但是这个方法,我们根本不用, 所以浪费性能, 为了解决这个问题,我们应该要在需要使用这个方法的函数上添加,不是所有的都添加
可以改造addMethod方法, 在原型上添加函数,而不是实例上, 这样我们就可以达到类式的链式调用

Function.prototype.addMethod = function( name, fn ){
    this.prototype[name] = fn;
    return this;
};

var fn1 = function(){};

fn1.addMethod( 'checkUserName', function(){
    console.log( 'userName:ghostwu' );
    return this;
} ).addMethod( 'checkUserEmail', function(){
    console.log( 'userEmail' );
    return this;
} ).addMethod( 'checkUserPwd', function(){
    console.log( 'userUserPwd' );
    return this;
} );
new fn1().checkUserName().checkUserEmail().checkUserPwd();

JS对象式编程

20.parseInt和number的区别

parseInt()

parseInt()函数将给定的字符串以指定的基数解析为整数

parseInt(string,radix)

第二个参数表示使用的进制,我们一般使用10进制,也可能会有到8或者16进制。为了避免对“0”和“0x”开头的字符串解析错误,各种javascript编程规范都规定必须要明确给出第二个参数的值,如parseInt(“123”,10).
parseInt从头解析string为整数,在遇到不能解析的字符时就返回已经解析的整数部分,如果第一个字符就不能解析,就直接返回NaN。

Number()

Number()在不用new操作符时,可以用来执行类型转换。如果无法转换为数字,就返回NaN。
像“123a”,parseInt()返回是123,Number()返回是NaN。

不同类型的字符串使用这两个函数的转换区别:

// 当字符串是由数字组成的时候 他们转换的数字一样的没有差别  
let numStr = '123'
console.log(parseInt(numStr))   //123
console.log(Number(numStr))		//123

// 当字符串是由字母组成的时候 
let numStr = 'abc'
console.log(parseInt(numStr))   //NaN
console.log(Number(numStr))		//NaN

// 当字符串是由数字和字母组成的时候 
let numStr = '123a'
console.log(parseInt(numStr))   //123
console.log(Number(numStr))		//NaN

// 当字符串是由0和数字
let numStr = '0123'
console.log(parseInt(numStr))   //123
console.log(Number(numStr))		//123

// 当字符串包含小数点
let numStr = '123.456'
console.log(parseInt(numStr))		//123
console.log(Number(numStr))			//123.456

// 当字符串为null时
let numStr = null
console.log(parseInt(numStr))		//NaN
console.log(Number(numStr))			//0

// 当字符串为''(空)时
let numStr = ''
console.log(parseInt(numStr))		//NaN
console.log(Number(numStr))			//0

监听一个元素的变化

js监听页面元素变化window.MutationObserver
js怎么监听元素属性变化

使用proxy拦截set

JS中0.1+0.2是多少,如何解决精度问题

对于计算机而言,两个数字在相加时是以二进制形式进行的,在呈现结果时才转换成十进制。JS中的数字是用IEEE 754 双精度版本(64位)浮点数来存储的。

  1. 十进制小数转换为二进制小数:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
如:0.7=0.1 0110 0110...B
0.7*2=1.4========取出整数部分1
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0
0.4*2=0.8========取出整数部分0
0.8*2=1.6========取出整数部分1
0.6*2=1.2========取出整数部分1
0.2*2=0.4========取出整数部分0

 // 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011无限循环)

// 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011无限循环)

// 相加后
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十进制)

0.1 在二进制中是无限循环的一些数字,其实不只是 0.1,其实很多十进制小数用二进制表示都是无限循环的。这样其实没什么问题,但是 JS 采用的浮点数标准却会裁剪掉我们的数字。

  1. Number.prototype.toFixed()
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
  1. 通过decimal.js/bignumber.js/big.js库 + parseFloat()来解决

单精度/双精度

单精度

单精度是这样的格式,1位符号,8位指数,23位小数。
在这里插入图片描述

双精度

双精度是1位符号,11位指数,52位小数。
面试:JavaScript基础概念_第22张图片
JavaScript中数字的存储标准是 IEEE754 浮点数标准。代码中使用的无论是整数还是小数,都是同一种数据类型——64 位双精度浮点型。

所有数字以二进制存储,每个数字对应的二进制分为三段:符号位、指数位、尾数位

用于存储的二进制有 64 位,其中符号位在六十四位的第一位,0 表示正数,1 表示负数。符号位之后的 11 位是指数位,决定了数字的范围指数位之后的 52 位是尾数位,决定了数字的精度

在 JavaScript 中,双精度浮点型的数转化成二进制的数保存,读取时根据指数位和尾数位的值转化成双精度浮点数。

比如说存储 8.8125 这个数,它的整数部分的二进制是 1000,小数部分的二进制是 1101。这两部分连起来是 1000.1101,但是存储到内存中小数点会消失,因为计算机只能存储 0 和 1。

1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数。

现在我们很容易判断符号位是 0,尾数位就是科学计数法的小数部分 0001101。指数位用来存储科学计数法的指数,此处为 3。指数位有正负,11 位指数位表示的指数范围是 -1023~1024,所以指数 3 的指数位存储为 1026(3 + 1023)。

可以判断 JavaScript 数值的最大值为 53 位二进制的最大值: 2^53 -1。

PS:科学计数法中小数点前的 1 可以省略,因为这一位永远是 1。0.5 二进制科学计数为 1.00 * 2^-1。

setTimeout, setInterval

现在再看这倒题的话其实只要记住,setTimeout 和 setInterval 是在相应的时间把任务添加到任务队列,但不能保证它立即执行。记住这个点应该就能够解决这类问题。

setTimeout和setInterval的回调函数,都是经过n毫秒后被添加到队列中,而不是过n毫秒后立即执行

面试:JavaScript基础概念_第23张图片
上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。

综上所述,setInterval有两个缺点:

  • 使用setInterval时,某些间隔会被跳过;
  • 可能多个定时器会连续执行;

可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)

因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。

懒加载是怎么完成的(图片and组件)

减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。
面试:JavaScript基础概念_第24张图片
监听onscroll事件判断资源位置
如上图所示,让在浏览器可视区域的图片显示,可视区域外的不显示,所以当图片距离顶部的距离top-height等于(小于)可视区域h和滚动区域高度s之和时说明图片马上就要进入可视区了,就是说当top-height<=s+h时,图片在可视区。

首先为所有懒加载的静态资源添加自定义属性字段,比如如果是图片,可以指定data-src为真实的图片地址,src指向loading的图片。
然后当资源进入视口的时候,将src属性值替换成data-src的值。
可以使用元素的getBoundingRect().top判断是否在视口内,也可以使用元素距离文档顶部的距离img.offsetTop减去scrollTop是否小于视口高度来判断。

API函数:
页可见区域宽: document.body.clientWidth;
网页可见区域高: document.body.clientHeight;
网页可见区域宽: document.body.offsetWidth (包括边线的宽);
网页可见区域高: document.body.offsetHeight (包括边线的宽);
网页正文全文宽: document.body.scrollWidth;
网页正文全文高: document.body.scrollHeight;
网页被卷去的高: document.body.scrollTop;
网页被卷去的左: document.body.scrollLeft;
网页正文部分上: window.screenTop;
网页正文部分左: window.screenLeft;
屏幕分辨率的高: window.screen.height;
屏幕分辨率的宽: window.screen.width;
屏幕可用工作区高度: window.screen.availHeight;

DOM事件流

dom事件机制、DOM0,DOM2,DOM3

垃圾回收

如果一个发起了两个请求,第二个请求先返回,第一个请求后返回,这种情况下怎么保证展示最新数据?

维护给每个请求/响应带上序列号,每次只会使用当前收到响应序列号最大的数据。

for循环 + setTimeout

  1. IIFE
for(let i = 0; i < 5; i++) {
    (function(j) {
        setTimeout(() => {
            console.log(j);
        }, 1000);
    })(i);
}
  1. 直接console.log
for(let i = 0; i < 5; i++) {
    setTimeout(console.log(i), 1000);
}

console.log()不为回调函数,则为同步函数和for循环一起执行

  1. 封装一下setTimeout
function timer(i) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}

for(let i = 0; i < 5; i++) {
    timer(i);
}

CommonJS与ES6模块的差异

  1. CommonJS 是运行时加载,ES6 模块是编译时输出接口
  2. CommonJS 输出的是一个值的复制,ES6 输出的是值的引用
  3. ES6 module 在编译期间会将所有 import 提升到顶部, commonjs 不会提升 require

==和 === 以及 Object.is() 的区别

== 允许在相等比较过程中进行强制类型转换,而 === 不允许。

  1. 字符串和数字之间的比较:字符串执行 ToNumber() 转化为数字
  2. 其他类型和布尔值之间的比较:布尔值执行 ToNumber() 转化为数字
  3. null只 == undefined,其他值和 null 或 undefined 比较均不相等
  4. 对象和基本类型的比较,对象执行 ToPrimitive() 转化为基本类型
    在 === 中, NaN !== NaN, -0 === +0

而在 Object.is 中,Object.is(NaN, NaN) -> true , Object.is(-0, +0) ->false,其余与 === 一致

实现一个 Object.is:

Object.prototype.is = function(a, b) {
    if(a === b) {
        // 可能为 -0 +0
        return x !== 0 || 1/x === 1/y
    }
    // x 与 y 都为 NaN
    return x !== x && y !== y
}
==判断流程
  1. 首先会判断两者类型是否相同。相同的话就是比大小了;
  2. 类型不相同的话,那么就会进行类型转换;
  3. 会先判断是否在对比null和undefined,是的话就会返回true;
  4. 判断两者类型是否为string和number,是的话就会将字符串转换为number;
1 == '1'1 ==  1
  1. 判断其中一方是否为boolean,是的话就会把boolean转为number再进行判断;
'1' == true'1' ==  11  ==  1
  1. 判断其中一方是否为object且另一方为string、number或者symbol,是的话就会把object转为原始类型再进行判断。
'1' == { name: 'test' }'1' == '[object Object]'

面试:JavaScript基础概念_第25张图片

图片上传

const addImageDG = (event) => {
    if (event.target.files[0]) {
        let reader = new FileReader();
        let file = event.target.files[0];
        const x = Math.floor(Math.random() * 500)
        const y = Math.floor(Math.random() * 400 + 80)
        reader.readAsDataURL(file);
        reader.onload = function (e) {
            var image = new Image();
            image.src = e.target.result;
            image.onload = function () {
                var imageCanvas = document.getElementById('imgCanvas');
                var context = imageCanvas.getContext('2d');
                const { width, height } = resizeImg(image);
                const canvasWidth = getCanvasSizeG().width;
                const canvasHeight = getCanvasSizeG().height;
                context.drawImage(image, x, y, width, height);
                //let imgData = imageCanvas.toDataURL("image/jpeg");
                protocol.addImageDL2(x / canvasWidth, y / canvasHeight, width / canvasWidth, height / canvasHeight, e.target.result);
            } 
        }
    }
}

你可能感兴趣的:(基础知识,javascript,前端)