JavaScript专题——深度解析中

236 阅读6分钟

1、原始类型有哪几种?null是对象吗?原始数据类型和复杂数据类型存储有什么区别?

a. 原始类型有6种,分别是undefined/null/bool/string/number/symbol(ES6新增)

b. 虽然typeof null返回的值是object,但是null不是对象,而是基本数据类型的一种。

c. 原始数据类型存储在栈内存,存储的是值。

d. 复杂数据类型存储在堆内存,存储的是地址。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。

2、typeof是否正确判断类型? instanceof呢?instanceof的实现原理是什么?

typeof能够正确判断基本数据类型,除了null;instanceof可以准确判断复杂数据类型,除了function。instanceof是通过原型链判断的,A instanceof B, 在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null;即Object.prototype.__proto__),仍然不等于B.prototype,那么返回false,否则返回true。

instanceof的实现代码:

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
  var O = R.prototype;// 取 R 的显式原型
  L = L.__proto__;    // 取 L 的隐式原型
  
  while (true) {
    if (L === null) return false; //已经找到顶层
    if (O === L) return true; //当 O 严格等于 L 时,返回 true
    L = L.__proto__;  //继续向上一层原型链查找
  }
}

3、for of、for in、forEach、map的区别?

a. for...of循环:具有iterator接口,就可以用for...of循环遍历它的成员(属性值)for...of循环可以使用的范围包括数组、SetMap结构、某些类似数组的对象、Generator 对象,以及字符串。for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。对于普通的对象,for...of结构不能直接使用,会报错,必须部署了Iterator接口后才能使用。可以中断循环。

b. for...in循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。

c. forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)

d. map: 只能遍历数组,不能中断,返回值是修改后的数组。

示例:

let arry = [1, 2, 3, 4];
arry.forEach((item) => {
  item *= 10;
});

console.log(arry); //[1, 2, 3, 4]
arry.forEach((item) => {
  arry[1] = 10; //直接操作数组
});

console.log(arry); //[ 1, 10, 3, 4 ]
let arry2 = [
  { name: "Yve" },
  { age: 20 }
];

arry2.forEach((item) => {
  item.name = 10;
});

console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]

4、如何判断一个变量是不是数组?

a. 使用Array.isArray判断,如果返回true, 说明是数组。

b. 使用 instanceof Array 判断,如果返回true, 说明是数组。

c. 使用Object.prototype.toString.call判断,如果值是 [object Array], 说明是数组。

d. 通过constructor来判断,如果是数组,那么arr.constructor === Array (不准确,因为我们可以指定 obj.constructor = Array)

示例:

function fn() {
  console.log(Array.isArray(arguments)); //false;
  console.log(Array.isArray([1,2,3,4])); //true
  console.log(arguments instanceof Array); //fasle
  console.log([1,2,3,4] instanceof Array); //true
  console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
  console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
  console.log(arguments.constructor === Array); //false
  arguments.constructor = Array;
  console.log(arguments.constructor === Array); //true
  console.log(Array.isArray(arguments)); //false
}
fn(1,2,3,4);

5、类数组和数组的区别是什么?

a. 类数组拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理)。

b. 不具有数组所具有的方法。

c. 类数组是一个普通对象,而真实的数组是Array类型。

常见的类数组: arugments/NodeList/jQuery对象 (比如$("div"))

类数组可以转换为数组:

//第一种方法
Array.prototype.slice.call(arrayLike, start);
//第二种方法
[...arrayLike];
//第三种方法:
Array.from(arrayLike);

注意事项:

a. 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

b. Array.from方法可以将类数组对象和可遍历对象转为真正的数组。

6、==和===有什么区别?

a. ===不需要进行类型转换,只有类型相同并且值相等时,才返回true

b. ==如果两者类型不同,首先需要进行类型转换。具体流程如下:

【step1】首先判断两者类型是否相同,如果相等,判断值是否相等。

【step2】如果类型不同,进行类型转换。

【step3】判断比较的是否是null或者是undefined, 如果是, 返回true

【setp4】判断两者类型是否为stringnumber, 如果是, 将字符串转换成number

【step5】判断其中一方是否为boolean,如果是, boolean转为number再进行判断。

【step6】判断其中一方是否为object且另一方为stringnumber或者symbol,如果是, object转为原始类型再进行判断。

示例:

let person1 = {
  age: 25
};

let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); // true,注意复杂数据类型,比较的是引用地址
[] == ![] // true

7、ES6中的class和ES5的类有什么区别?

a. ES6 class内部所有定义的方法都是不可枚举的。

b. ES6 class必须使用new调用。

c. ES6 class不存在变量提升。

d. ES6 class默认即是严格模式。

e. ES6 class子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

8、数组的哪些API会改变原数组?

修改原数组的API有:splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

不修改原数组的API有:slice/map/forEach/every/filter/reduce/entries/find

9、let、const以及var的区别是什么?

a. letconst定义的变量不会出现变量提升,而var定义的变量会提升。

b. letconstJS中的块级作用域。

c. letconst不允许重复声明(会抛出错误)

d. letconst定义的变量不能用在定义语句之前,如果使用会抛出错误,而var不会。

e. const声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)

10、在JS中什么是变量提升?什么是暂时性死区?

变量提升就是变量在声明之前就可以使用,值为undefined;在代码块内,使用let/const命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着typeof不再是一个百分百安全的操作。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

示例:

typeof x; // ReferenceError(暂时性死区,抛错)
let x;
typeof y; // 值是undefined,不会报错

11、如何正确的判断this? 箭头函数的this是什么?

a. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。

b. 函数是否通过call,apply调用,或者使用了bind (即硬绑定),如果是,那么this绑定的就是指定的对象。

c. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this 绑定的是那个上下文对象。一般是 obj.foo()

d. 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到 undefined,否则绑定到全局对象。

e. 如果把null或者undefined作为this的绑定对象传入callapply或者bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。

f. 箭头函数没有自己的this, 它的this继承于上一层代码块的this

12、词法作用域和this的区别?

a. 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。

b. this是在调用时被绑定的,this指向什么,完全取决于函数的调用位置。

13、谈谈你对JS执行上下文栈和作用域链的理解?

执行上下文:就是当前JavaScript代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。

作用域链:无论是LHS还是RHS查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。

a. JavaScript执行在单线程上,所有的代码都是排队执行。

b. 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。

c. 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。

d. 浏览器的JS执行引擎总是访问栈顶的执行上下文。

e. 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

14、什么是闭包?闭包的作用是什么?闭包有哪些使用场景?

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包最常用的方式就是在一个函数内部创建另一个函数。

闭包的作用:封装私有变量、模仿块级作用域、实现JS的模块。

15、call、apply有什么区别?call、aplly和bind的内部是如何实现的?

a. fn.call(obj, arg1, arg2, ...),调用一个函数, 具有一个指定的this值和分别地提供的参数(参数的列表)

b. fn.apply(obj, [argsArray]),调用一个函数,具有一个指定的this值,以及作为一个数组(或类数组对象)提供的参数。

callapply实现思路:

【step1】将函数设为传入参数的属性

【step2】指定this到函数并传入给定参数执行函数

【step3】如果不传入参数或者参数为null,默认指向为 window / global

【step4】删除参数上的函数

示例:

Function.prototype.call = function(context) {
  if (!context) {
    // context为null或者是undefined
    context = typeof window === 'undefined' ? global : window;
  }

  context.fn = this; // this指向的是当前的函数(Function的实例)
  let args = [...arguments].slice(1);
  let result = context.fn(...args); //隐式绑定,当前函数的this指向了context
  delete context.fn;
  return result;
}

Function.prototype.apply = function(context, rest) {
  if (!context) {
    // context为null或者是undefined时,设置默认值
    context = typeof window === 'undefined' ? global : window;
  }

  context.fn = this;
  let result;
  if(rest === undefined || rest === null) {
    // undefined 或者 是null不是Iterator 对象,不能被...
    result = context.fn(rest);
  }else if(typeof rest === 'object') {
    result = context.fn(...rest);
  }

  delete context.fn;
  return result;
}

bind的实现思路:

bind和call/apply有一个很重要的区别,一个函数被call/apply的时候,会直接调用,但是 bind会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

示例:

Function.prototype.my_bind = function(context) {
  if(typeof this !== "function"){
    throw new TypeError("not a function");
  }

  let self = this;
  let args = [...arguments].slice(1);
  function Fn() {};
  Fn.prototype = this.prototype;
  
  let bound = function() {
    let res = [...args, ...arguments]; //bind传递的参数和函数调用时传递的参数拼接
    context = this instanceof Fn ? this : context || this;
    return self.apply(context, res);
  }

  //原型链
  bound.prototype = new Fn();
  return bound;
}

16、new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别?

字面量创建对象,不会调用Object构造函数, 简洁且性能更好。

new的原理:

a. 创建一个新对象。

b. 这个新对象会被执行原型连接。

c. 将构造函数的作用域赋值给新对象,即this指向这个新对象.

d. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

示例:

function new(func) {
  let target = {};
  target.__proto__ = func.prototype;
  let res = func.call(target);
  if(typeof(res) == "object" || typeof(res) == "function") {
      return res;
  }
  return target;
}

17、谈谈你对原型的理解?

在JavaScript中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。

18、什么是原型链?

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

示例:p.__proto__ === Parent.prototype

19、prototype和__proto__区别是什么?

a. prototype是构造函数的属性。

b. __proto__是每个实例都有的属性,可以访问 [[prototype]] 属性。

c. 实例的__proto__与其构造函数的prototype指向的是同一个对象。

示例:

function Student(name) {
  this.name = name;
}

Student.prototype.setAge = function(){
  this.age=20;
}

let Jack = new Student('jack');
console.log(Jack.__proto__);
console.log(Student.prototype);
console.log(Jack.__proto__ === Student.prototype);//true

20、取数组的最大值(ES5、ES6)?

// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);
// ES6 的写法
Math.max(...[14, 3, 77, 30]);
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
  return accumulator = accumulator > currentValue ? accumulator : currentValue;
});

21、ES6新的特性有哪些?

a. 新增了块级作用域(let, const)

b. 提供了定义类的语法糖(class)

c. 新增了一种基本数据类型(Symbol)

d. 新增了变量的解构赋值

e. 函数参数允许设置默认值,引入了rest参数,新增了箭头函数

f. 数组新增了一些API,如isArray/from/ of方法,数组实例新增了entries()keys()values() 等方法

g. 对象和数组新增了扩展运算符

h. ES6新增了模块化(import/export)

i. ES6新增了 Set Map 数据结构

j. ES6原生提供Proxy构造函数,用来生成Proxy实例

k. ES6新增了生成器(Generator)和遍历器(Iterator)

22、为什么0.1 + 0.2 !=0.3 ?

0.1 + 0.2 != 0.3是因为在进制转换和进阶运算的过程中出现精度损失。

进制转换:

0.1 -> 0.0001100110011001...(无限循环)

0.2 -> 0.0011001100110011...(无限循环)

但是由于IEEE 754尾数位数限制,需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

对阶运算:由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失。

23、promise有几种状态,Promise有什么优缺点?

promise有三种状态:fulfilledrejectedpending

Promise 的优点:

a. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

b. 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise 的缺点:

a. 无法取消Promise

b. 当处于pending状态时,无法得知目前进展到哪一个阶段。

24、Promise构造函数是同步还是异步执行,then中的方法呢?promise如何实现then处理?

Promise的构造函数是同步执行的,then中的方法是异步执行的。

25、Promise和setTimeout的区别?

Promise是微任务,setTimeout是宏任务,同一个事件循环中,promise总是先于setTimeout 执行。

26、说一说JS异步发展史?

【step1】回调函数: callback

【step2】Promise

【step3】Generator

【step4】async/await

回调函数的使用场景:事件回调、Node API、setTimeout/setInterval中的回调函数。

异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能try catch和回调地狱。

示例:

fs.readFile(A, 'utf-8', function(err, data){
  fs.readFile(B, 'utf-8', function(err, data) {
    fs.readFile(C, 'utf-8', function(err, data) {
      fs.readFile(D, 'utf-8', function(err, data) {
        //....
      });
    });
  });
});

Promise主要解决了回调地狱的问题,可以使用bluebird将接口promise化。

使用Promise库来实现:

function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if(err) reject(err);
      resolve(data);
    });
  });
}

read(A).then(data => {
  return read(B);
}).then(data => {
  return read(C);
}).then(data => {
  return read(D);
}).catch(reason => {
  console.log(reason);
});

使用Generator + co库来实现:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
  yield readFile(A, 'utf-8');
  yield readFile(B, 'utf-8');
  yield readFile(C, 'utf-8');
}

co(read()).then(data => {
  //code
}).catch(err => {
  //code
});

自己写一个最简的my_co

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
  let info = yield readFile('./JS/Async/data/info.txt', 'utf-8');
  let base = yield readFile(info, 'utf-8');
  let age = yield readFile(base, 'utf-8');
  return age;
}

function my_co (it) {
  return new Promise((resolve, reject) => {
    function next(data) {
      let {value, done} = it.next(data);
      if(!done) {
        value.then(val => {
          next(val);
        }, reject);
      }else{
        resolve(value);
      }
    }

    next();
  });
}

my_co(read()).then(data => {
  console.log(data); //输出22
});

Generator函数一般配合yieldPromise 使用,Generator函数返回的是迭代器。

示例:

function* gen() {
  let a = yield 111;
  console.log(a);
  let b = yield 222;
  console.log(b);
  let c = yield 333;
  console.log(c);
  let d = yield 444;
  console.log(d);
}

let t = gen();
// next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
t.next(1); // 第一次调用next函数时,传递的参数无效
t.next(2); // a输出2;
t.next(3); // b输出2; 
t.next(4); // c输出3;
t.next(5); // d输出3;

async/await的优点是代码清晰,不用像Promise写很多 then 链,就可以处理回调地狱的问题,错误可以被try catch

示例:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

async function read() {
  await readFile(A, 'utf-8');
  await readFile(B, 'utf-8');
  await readFile(C, 'utf-8');
}

read().then((data) => {
  //code
}).catch(err => {
  //code
});

27、使用async/await需要注意什么?

a. await命令后面的Promise对象,运行结果可能是rejected,此时等同于async函数返回的 Promise对象被reject。因此需要加上错误处理,可以给每个await后的 Promise增加catch 方法;也可以将await的代码放在try...catch中。

b. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

c. await命令只能用在async函数之中,如果用在普通函数,会报错。

d. async函数可以保留运行堆栈。

示例:

async function f1() {
  await Promise.all([
    new Promise((resolve) => {
      setTimeout(resolve, 600);
    }),
    new Promise((resolve) => {
      setTimeout(resolve, 600);
    })
  ])
}

function b() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 200)
  });
}

function c() {
  throw Error(10);
}

const m = async () => {
  await b();
  c();
};

m();

28、JS类型转换的规则是什么?

a. 通过Number()parseInt()parseFloat()toString()String()Boolean(),进行强制类型转换。

b. 逻辑运算符(&&||!)、运算符(+-*/)、关系操作符(><<=>=)、相等运算符(==)或者if/while 的条件,可能会进行隐式类型转换。

Number()强制类型转换规则:

1】如果是布尔值,truefalse分别被转换为10

2】如果是数字,返回自身。

3】如果是 null,返回0

4】如果是 undefined,返回NAN

5】如果是字符串,遵循以下规则:

1)如果字符串中只包含数字(或者是0X/0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制。

2)如果字符串中包含有效的浮点格式,将其转换为浮点数值。

3)如果是空字符串,将其转换为0

4)如不是以上格式的字符串,均返回NaN

6】如果是Symbol,抛出错误。

7】如果是对象,则调用对象的valueOf()方法,然后依据前面的规则转换返回的值。如果转换的结果是NaN ,则调用对象的toString()方法,再次依照前面的规则转换返回的字符串值。

部分内置对象调用默认的valueOf的行为:

Array 数组本身

Boolean 布尔值

Date 从 UTC 1970 年 1 月 1 日午夜开始计算,到所封装的日期所经过的毫秒数

Function 函数本身

Number 数字值

Object 对象本身

String 字符串值

parseInt(param, radix)类型转换规则:

如果指定radix参数,以radix为基数进行解析。

如果第一个参数传入的是字符串类型:

a. 忽略字符串前面的空格,直至找到第一个非空字符,如果是空字符串,返回NaN

b. 如果第一个字符不是数字符号或者正负号,返回NaN

c. 如果第一个字符是数字/正负号,则继续解析直至字符串解析完毕或者遇到一个非数字符号为止。

如果第一个参数传入的Number类型:

数字如果是0开头,将其当作八进制来解析;如果以0x开头,将其当作十六进制来解析。

如果第一个参数是null或者是undefined,或者是一个对象类型:

a. 如果第一个参数是数组,去数组的第一个元素,按照上面的规则进行解析。

b. 如果第一个参数是Symbol类型,抛出错误。

parseFloat转换规则:

规则和parseInt基本相同,接受一个Number类型或字符串,如果是字符串中,那么只有第一个小数点是有效的。

toString()转换规则:

a. 如果是Number类型,输出数字字符串。

b. 如果是null或者是undefined,抛错。

c. 如果是数组,那么将数组展开输出。空数组,返回‘’。

d. 如果是对象,返回[object Object]

e. 如果是Date, 返回日期的文字表示法。

f. 如果是函数,输出对应的字符串。

g. 如果是Symbol,输出Symbol字符串。

String()转换规则:

String()的转换规则与toString()基本一致,最大的一点不同在于null和undefined,使用String进行转换,null和undefined对应的是字符串 'null' 和 'undefined'。

Boolean()转换规则:

除了undefined、null、false、''、0(包括 +0,-0)、NaN转换出来是false,其它都是true。

隐式类型转换:

a. &&/||/!/if/while的条件判断,需要将数据转换成Boolean类型,转换规则同Boolean强制类型转换。

b. 运算符: +-* /+号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当+号两边都是数字时,进行的是加法运算。如果两边都是字符串,直接拼接,无需进行隐式类型转换。除了上面的情况外,如果操作数是对象、数值或者布尔值,则调用toString()方法取得字符串值。对于undefinednull,分别调用String()显式转换为字符串,然后再进行拼接。

-*/ 操作符针对的是运算,如果操作值之一不是数值,则被隐式调用Number()函数进行转换。如果其中有一个转换除了为NaN,结果为NaN

示例:

console.log({}+10); // [object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);// 1,2,3,,5,610

关系操作符==>< <=>=

a. 如果两个操作值都是数值,则进行数值比较。

b. 如果两个操作值都是字符串,则比较字符串对应的字符编码值。

c. 如果有一方是Symbol类型,抛出错误。

d. 除了上述情况之外,都进行Number()进行类型转换,然后再进行比较。

注:NaN是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false

示例:

console.log(10 > {}); // 返回false.
/**
 * {}.valueOf ---> {}
 * {}.toString() ---> '[object Object]' ---> NaN
 * NaN和任何类型比大小,都返回false
 */

相等操作符==

a. 如果类型相同,无需进行类型转换。

b. 如果其中一个操作值是null或者是undefined,那么另一个操作符必须为null或者 undefined时,才返回true,否则都返回false

c. 如果其中一个是Symbol类型,那么返回false

d. 两个操作值是否为stringnumber,就会将字符串转换为number

e. 如果一个操作值是boolean,那么转换成number

f. 如果一个操作值为object且另一方为stringnumber或者symbol,是的话就会把object 转为原始类型再进行判断(调用objectvalueOf/toString方法进行转换)

对象如何转换成原始数据类型:

a. 如果部署了[Symbol.toPrimitive]接口,那么调用此接口,若返回的不是基础数据类型,抛出错误。

b. 如果没有部署[Symbol.toPrimitive]接口,那么先返回valueOf()的值,若返回的不是基础类型的值,再返回toString()的值,若返回的不是基础类型的值,则抛出异常。

示例:

//先调用 valueOf, 后调用 toString
let obj = {
  [Symbol.toPrimitive]() {
    return 200;
  },
  valueOf() {
    return 300;
  },
  toString() {
    return 'Hello';
  }
}

// 如果 valueOf 返回的不是基本数据类型,则会调用 toString,
// 如果 toString 返回的也不是基本数据类型,会抛出错误
console.log(obj + 200); // 400

29、简述下对webWorker的理解?

HTML5提出了Web Worker标准,表示js允许多线程,但是子线程完全受主线程控制并且不能操作dom,只有主线程可以操作dom,所以js本质上依然是单线程语言。web worker就是在js单线程执行的基础上开启一个子线程,进行程序处理,而不影响主线程的执行,当子线程执行完之后再回到主线程上,在这个过程中不影响主线程的执行。子线程与主线程之间提供了数据交互的接口postMessage和onmessage来进行数据发送和接收。

示例:

var worker = new Worker('./worker.js'); //创建一个子线程
worker.postMessage('Hello');

worker.onmessage = function (e) {
  console.log(e.data); //Hi
  worker.terminate(); //结束线程
};

//worker.js
onmessage = function (e) {
  console.log(e.data); //Hello
  postMessage("Hi"); //向主进程发送消息
};

30、js如何自定义事件?

【法1】使用new Event(),获取不到event.detail

示例:

let btn = document.querySelector('#btn');
let ev = new Event('alert', {
  bubbles: true, // 事件是否冒泡;默认值false
  cancelable: true, // 事件能否被取消;默认值false
  composed: false
});

btn.addEventListener('alert', function(event) {
  console.log(event.bubbles); // true
  console.log(event.cancelable); // true
  console.log(event.detail); // undefined
}, false);

btn.dispatchEvent(ev);

【法2】使用createEvent('CustomEvent'),要创建自定义事件,可以调用createEvent('CustomEvent'),返回的对象有initCustomEvent方法,接受以下四个参数:

1type: 字符串,表示触发的事件类型,如此处的'alert'

2bubbles: 布尔值: 表示事件是否冒泡

3cancelable: 布尔值,表示事件是否可以取消

4detail: 任意值,保存在 event 对象的 detail 属性中

示例:

let btn = document.querySelector('#btn');
let ev = btn.createEvent('CustomEvent');
ev.initCustomEvent('alert', true, true, 'button');

btn.addEventListener('alert', function(event) {
  console.log(event.bubbles); // true
  console.log(event.cancelable);// true
  console.log(event.detail); // button
}, false);

btn.dispatchEvent(ev);

【法3】使用 new customEvent(),使用起来比createEvent('CustomEvent')更加方便

示例:

var btn = document.querySelector('#btn');
var ev = new CustomEvent('alert', {
  bubbles: 'true',
  cancelable: 'true',
  detail: 'button'
});

btn.addEventListener('alert', function(event) {
  console.log(event.bubbles); // true
  console.log(event.cancelable);// true
  console.log(event.detail); // button
}, false);

btn.dispatchEvent(ev);

【法4】自定义非DOM事件(观察者模式)

1EventTarget类型有一个单独的属性handlers,用于存储事件处理程序(观察者)。

2addHandler()用于注册给定类型事件的事件处理程序;

3fire()用于触发一个事件;

4removeHandler() 用于注销某个事件类型的事件处理程序。

示例:

function EventTarget(){
  this.handlers = {};
}

EventTarget.prototype = {
  constructor: EventTarget,
  addHandler: function(type, handler){
    if (typeof this.handlers[type] === "undefined"){
      this.handlers[type] = [];
    }
    this.handlers[type].push(handler);
  },
  fire: function(event){
    if (!event.target){
      event.target = this;
    }

    if (this.handlers[event.type] instanceof Array){
      const handlers = this.handlers[event.type];
      handlers.forEach((handler)=>{
        handler(event);
      });
    }
  },
  removeHandler: function(type, handler){
    if (this.handlers[type] instanceof Array) {
      const handlers = this.handlers[type];
      for (var i = 0,len = handlers.length; i < len; i++){
        if (handlers[i] === handler){
          break;
        }
      }
      handlers.splice(i,1);
    }
  }
}

//使用
function handleMessage(event){
  console.log(event.message);
}

//创建一个新对象
var target = new EventTarget();
//添加一个事件处理程序
target.addHandler("message", handleMessage);
//触发事件
target.fire({type:"message", message:"Hi"}); //Hi
//删除事件处理程序
target.removeHandler("message",handleMessage);
//再次触发事件,没有事件处理程序
target.fire({type:"message",message: "Hi"});

31、跨域的方法有哪些?原理是什么?

【法1】jsonp

实现原理:

【step1】创建callback方法。

【step2】插入script标签。

【step3】后台接受到请求,解析前端传过去的callback方法,返回该方法的调用,并且数据作为参数传入该方法。

【step4】前端执行服务端返回的方法调用。

示例:

function jsonp({url, params, cb}) {
  return new Promise((resolve, reject) => {
    //创建script标签
    let script = document.createElement('script');
    
    //将回调函数挂在 window 上
    window[cb] = function(data) {
      resolve(data);
      //代码执行后,删除插入的script标签
      document.body.removeChild(script);
    }
    
    //回调函数加在请求地址上
    params = {...params, cb} //wb=b&cb=show
    let arrs = [];
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`);
    }

    script.src = `${url}?${arrs.join('&')}`;
    document.body.appendChild(script);
  });
}

【法2】cors

a. 简单跨域请求,只要服务器设置的Access-Control-Allow-Origin Header和请求来源匹配,浏览器就允许跨域。

示例:

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'XXXX');
});

b. 带预检的跨域请求,服务端需要设置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers

示例:

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'XXX');
  res.setHeader('Access-Control-Allow-Headers', 'XXX'); //允许返回的头
  res.setHeader('Access-Control-Allow-Methods', 'XXX');//允许使用put方法请求接口
  res.setHeader('Access-Control-Max-Age', 6); //预检的存活时间

  if(req.method === "OPTIONS") {
    res.end(); //如果method是OPTIONS,不做处理
  }
});

【法3】nginx反向代理

使用nginx反向代理实现跨域,只需要修改nginx的配置即可解决跨域问题。

示例:

server {
  listen       8090;
  server_name  localhost;
  location / {
    root  /Users/liuyan35/Test/Study/CORS/1-jsonp;
    index  index.html index.htm;
  }
  location /say {
    rewrite  ^/say/(.*)$ /$1 break;
    proxy_pass  
    http://localhost:3000;
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  }
  # others
}

【法4】websocket

Websocket是HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。Websocket不受同源策略影响,只要服务器端支持,无需任何配置就支持跨域。

示例:

// 前端
let socket = new
WebSocket('ws://localhost:3000'); //协议是ws

socket.onopen = function() {
  socket.send('Hi,你好');
}

socket.onmessage = function(e) {
  console.log(e.data)
}

// 后端
let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3000});
wss.on('connection', function(ws) {
  ws.on('message', function(data) {
    console.log(data); //接受到页面发来的消息'Hi,你好'
    ws.send('Hi'); //向页面发送消息
  });
});

【法5】postMessage

postMessage通过用作前端页面之前的跨域,如父页面与iframe页面的跨域。window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

子页面向父页面发消息:

// 父页面
window.addEventListener('message', (e) => {
  this.props.movePage(e.data);
}, false);

// 子页面(iframe):
if(/*左滑*/) {
    window.parent && window.parent.postMessage(-1, '*')
}else if(/*右滑*/){
    window.parent && window.parent.postMessage(1, '*')
}

父页面向子页面发消息:

// 父页面:
let iframe = document.querySelector('#iframe');
iframe.onload = function() {
  iframe.contentWindow.postMessage('hello', 'http://localhost:3002');
}

// 子页面:
window.addEventListener('message', function(e) {
  console.log(e.data);
  e.source.postMessage('Hi', e.origin); //回消息
});

【法6】document.domain

通过对domain设置当前域名来实现跨域,仅限于域名不同,但又要属于同一个基础域名下,如a.baidu.comb.baidu.com这2个子域名之间才能使用domain跨域,domain只能赋值为当前域名或基础域名,通过设置为同源域名(只能为基础域名),通过iframe操作另一个页面的内容。

<!-- test.html -->
<script>
document.domain = 'baidu.com';
const ifr = document.createElement('iframe');
ifr.src = 'a.baidu.com/test.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
  var doc = ifr.contentDocument || ifr.contentWindow.document;
  // 此处即可操作domain.html的document
  ifr.onload = null;
};
</script>

<!-- domain.html -->
<script>
  // domain.html下设置为与test.html中的domain一致
  document.domain = 'baidu.com';
</script>

【法7】window.name

window.name属于全局属性,在html中的iframe加载新页面(可以是跨域),通过iframe设置的src指向的源中更改name的值,同时主页面中的name也随之更改,但是需要给iframe中的window设置为about:blank或同源页面即可。

iframe使用之后应该删除,name的值只能为string类型,且数据量最大支持2MB。

<!-- test.html -->
// 封装应该用于获取数据的函数
function foo(url, func) {
  let isFirst = true;
  const ifr = document.createElement('iframe');
  loadFunc = () => {
    if (isFirst) {
      // 设置为同源
      ifr.contentWindow.location = 'about:blank';
      isFirst = false;
    } else {
      func(ifr.contentWindow.name);
      ifr.contentWindow.close();
      document.body.removeChild(ifr);
    }
  }
  ifr.src = url;
  ifr.style.display = 'none';
  document.body.appendChild(ifr);
  // 加载之后的回调
  ifr.onload = loadFunc;
}

foo(`http://127.0.0.1:5501/name.html`, (data) => {
  console.log(data)
})

<!-- name.html -->
const obj = { name: "iframe" };
// 修改name的值,必须为string类型
window.name = JSON.stringify(obj);

32、实现双向绑定Proxy与Object.defineProperty相比优劣如何?

a. Object.definedProperty的作用是劫持一个对象的属性,劫持属性的gettesetter方法,在对象的属性发生变化时进行特定的操作。而Proxy劫持的是整个对象。

b. Proxy会返回一个代理对象,我们只需要操作新对象即可,而Object.defineProperty只能遍历对象属性直接修改。

c. Object.definedProperty不支持数组,更准确的说是不支持数组的各种API,因为如果仅仅考虑arry[i] = value 这种情况,是可以劫持的,但是这种劫持意义不大。而Proxy可以支持数组的各种API

d. 尽管Object.defineProperty有诸多缺陷,但是其兼容性要好于Proxy

示例:

// Object.definedProperty
let obj = {};
let temp = 'Yvette';

Object.defineProperty(obj, 'name', {
  get() {
    console.log("读取成功");
    return temp
  },
  set(value) {
    console.log("设置成功");
    temp = value;
  }
});

obj.name = 'Chris';
console.log(obj.name);

// Proxy
let obj = {
  name: 'Yvette', 
  hobbits: ['travel', 'reading'], 
  info: {
    age: 20,
    job: 'engineer'
  }
};

let p = new Proxy(obj, {
  get(target, key) {
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    if(key === 'length') return true; //如果是数组长度的变化,返回
    return Reflect.set([target, key, value]);
  }
});

p.name = 20; //设置成功
p.age = 20; //设置成功;
// 不需要事先定义此属性
p.hobbits.push('photography'); // 读取成功;注意不会触发设置成功
p.info.age = 18; // 读取成功;不会触发设置成功

33、Object.is()与比较操作符===、==有什么区别?

以下情况,Object.is认为是相等:

a. 两个值都是undefined

b. 两个值都是null

c. 两个值都是true或者都是false

d. 两个值是由相同个数的字符按照相同的顺序组成的字符串

e. 两个值指向同一个对象

f. 两个值都是数字并且都是正零+0或都是负零-0或都是NaN或都是除零和NaN外的其它同一个数字。

Object.is() 类似于 ===,但是有一些细微差别,如下:

a. NaNNaN 相等

b. -0+0不相等