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循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象、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】判断两者类型是否为string和number, 如果是, 将字符串转换成number。
【step5】判断其中一方是否为boolean,如果是, 将boolean转为number再进行判断。
【step6】判断其中一方是否为object且另一方为string、number或者symbol,如果是, 将 object转为原始类型再进行判断。
示例:
let person1 = {
age: 25
};
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); // true,注意复杂数据类型,比较的是引用地址
[] == ![] // true7、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. let和const定义的变量不会出现变量提升,而var定义的变量会提升。
b. let和const是JS中的块级作用域。
c. let和const不允许重复声明(会抛出错误)。
d. let和const定义的变量不能用在定义语句之前,如果使用会抛出错误,而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的绑定对象传入call、apply或者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值,以及作为一个数组(或类数组对象)提供的参数。
call和apply实现思路:
【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);//true20、取数组的最大值(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有三种状态:fulfilled、rejected、pending。
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函数一般配合yield或Promise 使用,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】如果是布尔值,true和false分别被转换为1和0。
【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()方法取得字符串值。对于undefined和null,分别调用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. 两个操作值是否为string和number,就会将字符串转换为number。
e. 如果一个操作值是boolean,那么转换成number。
f. 如果一个操作值为object且另一方为string、number或者symbol,是的话就会把object 转为原始类型再进行判断(调用object的valueOf/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); // 40029、简述下对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方法,接受以下四个参数:
【1】type: 字符串,表示触发的事件类型,如此处的'alert'
【2】bubbles: 布尔值: 表示事件是否冒泡
【3】cancelable: 布尔值,表示事件是否可以取消
【4】detail: 任意值,保存在 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事件(观察者模式)
【1】EventTarget类型有一个单独的属性handlers,用于存储事件处理程序(观察者)。
【2】addHandler()用于注册给定类型事件的事件处理程序;
【3】fire()用于触发一个事件;
【4】removeHandler() 用于注销某个事件类型的事件处理程序。
示例:
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-Origin、Access-Control-Allow-Methods和Access-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.com与b.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的作用是劫持一个对象的属性,劫持属性的gette和setter方法,在对象的属性发生变化时进行特定的操作。而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. NaN和NaN 相等
b. -0和+0不相等