JavaScript专题—深度解析上

440 阅读27分钟

1、如何正确判断this的指向?

1】全局环境中的this—默认绑定

非严格模式:node环境,执行全局对象global,浏览器环境,执行全局对象 window。

严格模式:执行 undefined。

示例:

function info(){
console.log(this.age);
}
var age = 28;
info();

2】是否是new绑定

new的实现原理:

a. 创建一个新对象。

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

c. 属性和方法被加入到 this 引用的对象中。并执行了构造函数中的方法.

d. 如果函数没有返回其他对象,那么this 指向这个新对象,否则 this 指向构造函数中返回的对象。

function new(func) {
  let target = {};
  target.__proto__ = func.prototype;
  let res = func.call(target);
  //排除 null 的情况
  if (res && typeof(res) == "object" || typeof(res) == "function") {
     return res;
  }
  return target;
}

示例:

构造函数返回值不是 functionobject

function Super(age) {
    this.age = age;
}

let instance = new Super('26');
console.log(instance.age); //26 构造函数返回值是 function或object,这种情况下 this 指向的是返回的对象:

function Super(age) {
  this.age = age;
  let obj = {a: '2'};
  return obj;
}

let instance = new Super('hello');
console.log(instance.age); //undefined

3callapplybind显式绑定

示例:

function info(){
    console.log(this.age);
}

var person = {
  age: 20,
  info
};

var age = 28;
var info = person.info;

info.call(person); //20
info.apply(person); //20
info.bind(person)(); //20

如果 call,apply 或者bind 传入的第一个参数值是 undefined 或者null,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window。

示例:

function info(){
  console.log(this);
  console.log(this.age);
}

var person = {
  age: 20,
  info
};

var age = 28;
var info = person.info; 
info.call(null);

4xxx.fn()隐式绑定

示例:

function info(){
  console.log(this.age);
}

var person = {
  age: 20,
  info
};

var age = 28;
person.info(); //20;执行的是隐式绑定

5】箭头函数的情况

箭头函数没有自己的this,继承外层上下文绑定的this。

示例:

let obj = {
  age: 20,
  info: function() {
    return () => {
        console.log(this.age); //this继承的是外层上下文绑定的this
        }
  }
}

let person = {age: 28};
let info = obj.info();
info(); //20
let info2 = obj.info.call(person);
info2(); //28

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

目前,JS原始类型有六种,分别为:

Boolean/String/Number/Undefined/Null/Symbol(ES6新增)/ BigInt(ES10新增)

复杂数据类型只有一种:Object

null不是一个对象,尽管typeof null 输出的是 object,这是一个历史遗留问题,JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,null 表示为全零,所以将它错误的判断为object。

基本数据类型和复杂数据类型的区别为:

1】内存的分配不同

基本数据类型存储在栈中;复杂数据类型存储在堆中,栈中存储的变量,是指向堆中的引用地址。

2】访问机制不同

基本数据类型是按值访问;复杂数据类型按引用访问,JS不允许直接访问保存在堆内存中的对象,在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值。

3】复制变量时不同

基本数据类型a=b是将b中保存的原始值的副本赋值给新变量a,a和b完全独立,互不影响;复杂数据类型a=b是将b保存的对象内存的引用地址赋值给了新变量a;a和b指向了同一个堆内存地址,其中一个值发生了改变,另一个也会改变。

示例:

let b = {
  age: 10
};

let a = b;
a.age = 20;
console.log(a); //{ age: 20 }

4】参数传递的不同(实参/形参)

函数传参如果是基本数据类型,是按值传递,拷贝的是值;如果是复杂数据类型,拷贝的是引用地址。

示例:

//基本数据类型
let b = 10;
function change(info) {
  info=20;
}
//info=b;基本数据类型,拷贝的是值得副本,二者互不干扰
change(b);
console.log(b);//10

//复杂数据类型
let b = {
  age: 10
};

function change(info) {
  info.age = 20;
}

//info=b;根据第三条差异,可以看出,拷贝的是地址的引用,修改互相影响。
change(b);
console.log(b);//{ age: 20 }

3、说一说你对HTML5语义化的理解?

HTML5的语义化指的是合理正确的使用语义化的标签来创建页面结构,如 header、footer、nav,从标签上即可以直观的知道这个标签的作用,而不是滥用div。

语义化的优点有:

a. 代码结构清晰,易于阅读,利于开发和维护

b. 方便其他设备解析,如屏幕阅读器根据语义渲染网页。

c. 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

语义化标签主要有:

header/nav/aside/main/article/section/footer/address/title/h1~h6/ul/ol/canvas/dialog/figure/details/mark

4、如何让(a == 1&&a == 2&&a == 3)的值为true?

a == 1 && a == 2 && a == 3意味着其不可能是基本数据类型。因为如果a是null或者是 undefined或者是bool类型,都不可能返回true,因此可以推测 a 是复杂数据类型。

解决思路:将Object转换为原始类型

示例:

var obj = {
  [Symbol.toPrimitive](hint) {
    return 10;
  },
  valueOf() {
    return 20;
  },
  toString() {
    return 'hello';
  }
}

注意事项:

a. 如果hintnumber,调用顺序为:valueOf >>> toString

b. 如果hintstring,调用顺序为:toString >>> valueOf,即toString返回的不是基本数据类型,才会继续调用valueOf,如果valueOf返回的还不是基本数据类型,那么抛出错误。

c. 如果hintdefault,调用顺序为:valueOf >>> toString,即valueOf 返回的不是基本数据类型,才会继续调用toString,如果toString 返回的还不是基本数据类型,那么抛出错误。

【法1】部署[Symbol.toPrimitive]接口

let a = {
  [Symbol.toPrimitive]: (function(hint) {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function() {
      return i++;
    }
    })()
};

【法2】部署valueOf接口

let a = {
  valueOf: (function() {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function() {
      return i++;
    }
  })()
};

【法3】利用正则

let a = {
  reg: /\d/g,
  valueOf () {
    return this.reg.exec(123)[0]
  }
}

【法4】利用数据劫持

let i = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return i++;
  }
});
 
let a = new Proxy({}, {
  i: 1,
  get: function () {
    return () => this.i++;
  }
});

【法5】重写数组的join方法,数组的toString接口默认调用数组的join方法

let a = [1, 2, 3];
a.join = a.shift;

5、防抖和节流

防抖:n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。

节流:高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。

防抖的应用场景:每次resize/scroll触发统计事件、文本输入的验证。

函数节流的应用场景有:

a. DOM元素的拖拽功能实现(mousemove

b. 射击游戏的mousedown/keydown事件(单位时间只能发射一颗子弹)

c. 计算鼠标移动的距离(mousemove

d. Canvas模拟画板功能(mousemove

e. 搜索联想(keyup

f. 监听滚动事件判断是否到页面底部自动加载更多:给 scroll加了debounce后,只有用户停止滚动后,才会判断是否到了页面底部;如果是throttle的话,只要页面滚动就会间隔一段时间判断一次。

1】防抖函数实现:

immediate为true 时,表示开始会立即触发一次。

immediate为false 时,表示最后一次一定会触发。

loadsh中的debounce的第三个参数option,提供了leading和trailing两个参数。

示例:

function debounce(func, wait,immediate=true) {
  let timeout, context, args;
  // 延迟执行函数
  const later = () => setTimeout(() => {
    timeout = null;
    if (!immediate) {
      func.apply(context, args);
      context = args = null;
    }
  }, wait);

  let debounced = function (...params) {
    if (!timeout) {
      timeout = later();
      if (immediate) {
        //立即执行
        func.apply(this, params);
      } else {
        //闭包
        context = this;
        args = params;
      }
    } else {
      clearTimeout(timeout);
      timeout = later();
    }
  };

  debounced.cancel = function () {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
};

2】节流函数实现:

【法1】利用时间戳实现

function throttle (func, delay) {
  var lastTime = 0;
  function throttled() {
    var context = this;
    var args = arguments;
    var nowTime = Date.now();
    if(nowTime > lastTime + delay) {
      func.apply(context, args);
      lastTime = nowTime;
    }
  }
  
  //防抖函数最终返回的是一个函数
  return throttled;
}

【法2】利用定时器实现

function throttle(func, delay) {
  var timeout = null;
  function throttled() {
    var context = this;
    var args = arguments;
    if(!timeout) {
      timeout = setTimeout(()=>{
        func.apply(context, args);
        clearTimeout(timeout);
        timeout=null
      }, delay);
    }
  }
    return throttled;
}

【法3】组合实现,允许设置第一次或者最后一次是否触发函数执行

function throttle (func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};
  
  var later = function () {
    previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function () {
    var now = Date.now() || new Date().getTime();
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 判断是否设置了定时器和trailing
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function () {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
}

6、说一说你对JS执行上下文栈和作用域链的理解?

JS执行上下文:全局执行上下文、函数执行上下文、eval函数执行上下文

执行上下文创建过程中,需要做以下几件事:

a. 创建变量对象,首先初始化函数的参数arguments,提升函数声明和变量声明。

b. 创建作用域链,在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。

c. 确定this的值。

作用域:全局作用域、函数作用域、块级作用域

执行上下文栈—执行栈:

a. 首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。

b. 当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。

作用域链:就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就宣布放弃。

7、什么是BFC?BFC的布局规则是什么?如何创建BFC?

BFC,即块格式化上下文,它的布局规则如下:

a. BFC内,盒子依次垂直排列。

b. BFC内,两个盒子的垂直距离由margin属性决定。属于同一个BFC的两个相邻Boxmargin会发生重叠。

c. BFC内,每个盒子的左外边缘接触内部盒子的左边缘,对于从右到左的格式,右边缘接触。即使在存在浮动的情况下也是如此,除非创建新的BFC

d. BFC的区域不会与float box重叠。

e. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。

f. 计算BFC的高度时,浮动元素也参与计算。


如何创建BFC:

a. 根元素

b. 浮动元素(float属性不为none

c. positionabsoluterelative

d. overflow不为visible的块元素

e. displayinline-blocktable-celltable-caption


BFC的应用:防止margin重叠、清除内部浮动、自适应多栏布局

8、let、const、var 的区别有哪些?

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

b. 相同作用域中,letconst不允许重复声明,var允许重复声明。

c. const声明变量时必须设置初始值。

d. const声明一个只读的常量,这个常量不可改变。

e. let/const声明的变量仅在块级作用域中有效。而var声明的变量在块级作用域外仍能访问到。

f. 顶层作用域中var声明的变量挂在window上。

g. let/const有暂时性死区的问题,即let/const声明的变量,在定义之前都是不可用的,如果使用会抛出错误。

示例:

{
  let a = 10;
  const b = 20;
  var c = 30;
}
console.log(a); //ReferenceError
console.log(b); //ReferenceError
console.log(c); //30

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = (function(j){
    return function () {
      console.log(j);
    }
  })(i)
}
a[6](); // 6

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

9、深拷贝和浅拷贝的区别是什么?如何实现一个深拷贝?

浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。

浅拷贝的实现:for in/Object.assign/.../Array.prototype.slice()/Array.prototype.concat()等。

示例:

let obj = {
  name: 'Yvette',
  age: 18,
  hobbies: ['reading', 'photography']
};

let obj2 = Object.assign({}, obj);
let obj3 = {...obj};
obj.name = 'Jack';
obj.hobbies.push('coding');
console.log(obj);//{ name: 'Jack', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj2);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
console.log(obj3);//{ name: 'Yvette', age: 18,hobbies: [ 'reading', 'photography', 'coding' ] }
 
let obj = {
  name: 'Yvette',
  age: 18,
  hobbies: ['reading', 'photography']
};

let newObj = {};
for(let key in obj){
  newObj[key] = obj[key];
}

console.log(newObj);// { name: 'Yvette', age: 18, hobbies: ['reading', 'photography' ] }
obj.age = 20;
obj.hobbies.pop();
console.log(newObj);// { name: 'Yvette', age: 18, hobbies: ['reading' ] }

深拷贝实现:JSON.parse(JSON.stringify(obj))/实现一个deepClone函数

JSON.parse(JSON.stringify(obj))实现方式的缺陷:

a. 对象的属性值是函数时,无法拷贝。

b. 原型链上的属性无法获取。

c. 不能正确的处理Date类型的数据。

d. 不能处理 RegExp

e. 会忽略symbol/undefined

示例:

let obj = {
  name: 'Yvette',
  age: 18,
  hobbies: ['reading', 'photography'],
  sayHi: function() {
    console.log(sayHi);
  }
}

let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);//{ name: 'Yvette',age: 18, hobbies: [ 'reading', 'photography' ] }
 
function Super() {}
Super.prototype.location = 'NanJing';

function Child(name, age, hobbies) {
  this.name = name;
  this.age = age;
}

Child.prototype = new Super();
let obj = new Child('Yvette', 18);
console.log(obj.location); //NanJing
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);//{ name: 'Yvette',age: 18}
console.log(newObj.location);//undefined;原型链上的属性无法获取

实现一个deepClone函数:

实现思路:

a. 如果是基本数据类型,直接返回。

b. 如果是RegExp或者Date类型,返回对应类型。

c. 如果是复杂数据类型,递归。

示例:

function deepClone(obj) { 
  if(obj instanceof RegExp) return new RegExp(obj);
  if(obj instanceof Date) return new Date(obj);
  if(obj === null || typeof obj !== 'object') {
    //如果不是复杂数据类型,直接返回
    return obj;
  }

  let t = new obj.constructor();

  for(let key in obj) {
    //如果 obj[key] 是复杂数据类型,递归
    if(obj.hasOwnProperty(key)){//是否是自身的属性
      t[key] = deepClone(obj[key]);
    }
  }
  return t;
}

10、什么是XSS攻击,XSS攻击可以分为哪几类?我们如何防范XSS攻击?

XSS,即跨站脚本攻击,是恶意代码未经过滤,与网站正常的代码混在一起,浏览器无法分辨哪些脚本是可信的,从而导致恶意脚本被执行的一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取cookie、session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等,

利用这些信息冒充用户向网站发起攻击者定义的请求。

XSS分类:反射型XSS、DOM 型 XSS、存储型XSS

1】反射型XSS

当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。

攻击步骤:

a. 攻击者构造出特殊的URL,其中包含恶意代码。

b. 用户打开带有恶意代码的URL时,网站服务端将恶意代码从URL中取出,拼接在HTML 中返回给浏览器。

c. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。

d. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

如何防范:对字符串进行编码

示例:

app.get('/welcome', function(req, res) {
  //对查询参数进行编码,避免反射型XSS攻击
  res.send(`${encodeURIComponent(req.query.type)}`);
});

2DOMXSS

DOM型XSS攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用innerHTML/outerHTML/appendChild/document.write()API时要特别小心,不要把不可信的数据作为HTML插到页面上,尽量使用innerText/textContent/setAttribute()等。

攻击步骤:

a. 攻击者构造出特殊数据,其中包含恶意代码。

b. 用户浏览器执行了恶意代码。

c. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

如何防范:

a. 对于url链接(例如图片的src属性),那么直接使用encodeURIComponent来转义。

b. url,我们可以这样进行编码:

function encodeHtml(str) {
  return str.replace(/"/g, '&quot;')
  .replace(/'/g, '&apos;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;');
}

3】存储型XSS

恶意脚本永久存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的原因仍然是没有做好数据过滤,前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出。

攻击步骤:

a. 攻击者将恶意代码提交到目标网站的数据库中。

b. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在HTML中返回给浏览器。

c. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。

d. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

如何防范:

a. 前端数据传递给服务器之前,先转义/过滤,但防范不了抓包修改数据的情况。

b. 服务器接收到数据,在存储到数据库之前,进行转义/过滤。

c. 前端接收到服务器传递过来的数据,在展示到页面前,先进行转义/过滤。

11、CSRF

CSRF跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

典型的CSRF攻击流程:

【Step1】受害者登录A站点,并保留了登录凭证(Cookie)。

【Step2】攻击者诱导受害者访问了站点B

【Step3】站点B向站点A发送了一个请求,浏览器会默认携带站点ACookie信息。

【Step4】站点A接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是无辜的受害者发送的请求。

【Step5】站点A以受害者的名义执行了站点B的请求。

【Step6】攻击完成,攻击者在受害者不知情的情况下,冒充受害者完成了攻击。

CSRF的特点:

a. 攻击通常在第三方网站发起,如上面的站点B,站点A无法防止攻击发生。

b. 利用受害者在被攻击网站的登录凭证,冒充受害者提交操作,并不会去获取cookie信息。

c. 跨站请求可以用各种方式:图片URL、超链接、CORSForm提交等等。

12、如何隐藏页面中的某个元素?

【法1】完全隐藏,元素从渲染树中消失,不占据空间

display: none;

【法2】视觉上的隐藏,屏幕中不可见,占据空间

position:absolute/fixed;// 可视区域不占位
left: -99999px;

position: relative; // 可视区域占位,如希望其在可视区域不占位,需同时设置height: 0;
left: -99999px;

margin-left: -99999px; //可视区域占位,如希望其在可视区域不占位,需同时设置height: 0;

height: 0;
width: 0;
font-size: 0;
 
height: 0;
width: 0;
overflow: hidden;

利用transfrom:
transform: scale(0);
transform: translateX(-99999px)/ translateY(-99999px);
transform: rotateY(90deg);
 
opacity: 0;
 
visibility: hidden
position: relative;
z-index: -999;
 
clip-path: polygon(0 0, 0 0, 0 0, 0 0); // 裁剪

【法3】语义上的隐藏,读屏软件不可读,但正常占据空

<div aria-hidden="true"></div> // 读屏软件不可读,占据空间,可见

13、浏览器事件代理机制的原理是什么?

添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。

示例:

target.addEventListener(type, listener[,options]);
target.addEventListener(type, listener[,useCapture]);

options(对象) | 可选:

a. capture: Booleantrue表示在捕获阶段触发,false表示在冒泡阶段触发。默认是false

b. once: Booleantrue表示listener在添加之后最多只调用一次,listener会在其被调用之后自动移除。默认是 false

c. passive: Booleantrue表示listener 永远不会调用preventDefault()。如果listener仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。默认是false

useCapture(Boolean) | 可选:

useCapture 默认为false。表示冒泡阶段调用事件处理程序,若设置为true,表示在捕获阶段调用事件处理程序。

示例:

document.addEventListener('click', function(e) {
  console.log(e.target);
  console.log(e.eventPhase);
}, false);

14、setTimeout倒计时为什么会出现误差?

setTimeout()只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在setTimeout() 指定的时间执行。所以,setTimeout()的第二个参数表示的是最少时间,并非是确切时间。

HTML5标准规定了setTimeout()的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame()的效果要好于setTimeout();

示例:

function step(timestamp) {
  window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);

误差来源:

a. 没有考虑误差时间,如函数执行的时间/其它代码的阻塞

b. 没有考虑浏览器的休眠

如何减少setTimeout的误差:通过对下一次任务的调用时间进行修正,来减少误差。

示例:

let count = 0;
let countdown = 5000; //服务器返回的倒计时时间
let interval = 1000;
let startTime = new Date().getTime();
let timer = setTimeout(countDownStart,interval); //首次执行

//定时器测试
function countDownStart() {
  count++;
  const offset = new Date().getTime() - (startTime + count * 1000);
  const nextInterval = interval - offset; //修正后的延时时间
  
  if (nextInterval < 0) {
    nextInterval = 0;
  }
  
  countdown -= interval;
  
  if (countdown <= 0) {
    clearTimeout(timer);
  }else {
    timer = setTimeout(countDownStart, nextInterval);
  }
}

JS的运行机制:

【1】所有同步任务都在主线程上执行,形成一个执行栈。

【2】主线程之外,还存在任务队列。

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

【4】主线程不断重复上面的第三步。

15、什么是闭包?闭包的作用是什么?

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

闭包的作用:

a. 能够访问函数定义时所在的词法作用域,阻止其被回收。

b. 私有化变量。

c. 模拟块级作用域。

d. 创建模块。

示例:

function base() {
  let x = 10; //私有变量
  return {
    getX: function() {
      return x;
    }
  }
}

let obj = base();
console.log(obj.getX()); //10
 
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = (function(j){
    return function () {
      console.log(j);   
    }
    })(i);
}
a[6](); // 6
 
function coolModule() {
  let name = 'Yvette';
  let age = 20;
  function sayName() {
    console.log(name);
  }
   
  function sayAge() {
    console.log(age);
  }
  return {sayName, sayAge }
}

let info = coolModule();
info.sayName(); //'Yvette'

闭包的缺点:闭包会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

16、Promise的源码实现

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor) {
   let self = this;
   self.status = PENDING;
   self.onFulfilled = [];//成功的回调
   self.onRejected = []; //失败的回调
   function resolve(value) {
       if (self.status === PENDING) {
           self.status = FULFILLED;
           self.value = value;
           self.onFulfilled.forEach(fn => fn());
       }
   }
   function reject(reason) {
       if (self.status === PENDING) {
           self.status = REJECTED;
           self.reason = reason;
           self.onRejected.forEach(fn => fn());
       }
   }
   try {
       executor(resolve, reject);
   } catch (e) {
       reject(e);
   }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
   onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
   onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
   let self = this;
   let promise2 = new Promise((resolve, reject) => {
       if (self.status === FULFILLED) {
           setTimeout(() => {
               try {
                   let x = onFulfilled(self.value);
                   resolvePromise(promise2, x, resolve, reject);
               } catch (e) {
                   reject(e);
               }
           });
       } else if (self.status === REJECTED) {
           setTimeout(() => {
               try {
                   let x = onRejected(self.reason);
                   resolvePromise(promise2, x, resolve, reject);
               } catch (e) {
                   reject(e);
               }
           });
       } else if (self.status === PENDING) {
           self.onFulfilled.push(() => {
               setTimeout(() => {
                   try {
                       let x = onFulfilled(self.value);
                       resolvePromise(promise2, x, resolve, reject);
                   } catch (e) {
                       reject(e);
                   }
               });
           });
           self.onRejected.push(() => {
               setTimeout(() => {
                   try {
                       let x = onRejected(self.reason);
                       resolvePromise(promise2, x, resolve, reject);
                   } catch (e) {
                       reject(e);
                   }
               });
           });
       }
   });
   return promise2;
}
function resolvePromise(promise2, x, resolve, reject) {
   let self = this;
   if (promise2 === x) {
       reject(new TypeError('Chaining cycle'));
   }
   if (x && typeof x === 'object' || typeof x === 'function') {
       let used;
       try {
           let then = x.then;
           if (typeof then === 'function') {
               then.call(x, (y) => {
                   if (used) return;
                   used = true;
                   resolvePromise(promise2, y, resolve, reject);
               }, (r) => {
                   if (used) return;
                   used = true;
                   reject(r);
               });
           }else{
               if (used) return;
               used = true;
               resolve(x);
           }
       } catch (e) {
           if (used) return;
           used = true;
           reject(e);
       }
   } else {
       resolve(x);
   }
}
module.exports = Promise;

17、实现Promise.all方法?

Promise.all的特点:

1Promise.all的返回值是一个promise实例

a. 如果传入的参数为空的可迭代对象,会同步返回一个已完成状态的promise

b. 如果传入的参数中不包含任promise,会异步返回一个已完成状态的promise

c. 其它情况下,会返回一个处理中pending状态的promise

2Promise.all返回的promise 的状态

a. 如果传入的参数中的promise都变成完成状态,返回的promise异步地变为完成。

b. 如果传入的参数中,有一个promise失败,异步地将失败的那个结果给失败状态的回调函数,而不管其它promise是否完成。

c. 在任何情况下,返回的promise的完成状态的结果都是一个数组。

示例:

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    //Array.from 将可迭代对象转换成数组
    promises = Array.from(promises);
    
    if (promises.length === 0) {
      resolve([]);
    } else {
      let result = [];
      let index = 0;
      for (let i = 0; i < promises.length; i++ ) {
        //考虑到i可能是thenable对象也可能是普通值
        Promise.resolve(promises[i]).then(data => {
          result[i] = data;
          if (++index === promises.length) {
            resolve(result);
          }
        }, err => {
          reject(err);
          return;
        });
      }
    }
  });
}

18、实现Promise.race方法?

Promise.race的特点:Promise.race的返回值是一个promise实例

a. 如果传入的参数为空的可迭代对象,会返回一个处理pendingpromise

b. 如果传入的参数中不包含任何promise,会返回一个处理pendingpromise

c. 如果iterable包含一个或多个非promise值或已经解决的promise,则Promise.race将解析为iterable中找到的第一个值。

示例:

Promise.race = function (promises) {
  promises = Array.from(promises);//将可迭代对象转换为数组
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      //空的可迭代对象;
      //用于在pending态
    } else {
      for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i]).then((data) => {
          resolve(data);
        }).catch((reason) => {
          reject(reason);
        })
      }
    }
  });
}

19、如何实现Promise.finally?

不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then。并且会将值原封不动的传递给后面的then。

示例:

Promise.prototype.finally = function(callback) {
  return this.then((value) => {
    return Promise.resolve(callback()).then(() => {
      return value;
    });
  }, (err) => {
    return Promise.resolve(callback()).then(() => {
      throw err;
    });
  });
}

20、实现Promise.resolve方法

Promise.resolve = function (param) {
  if (param instanceof Promise) {
    return param;
  }
  
  return new Promise((resolve, reject) => {
    if (param && param.then && typeof param.then === 'function') {
      setTimeout(() => {
        param.then(resolve, reject);
      });
    } else {
      resolve(param);
    }
  });
}

21、Promise.reject方法的实现

Promise.reject = function (reason) {
   return new Promise((resolve, reject) => {
       reject(reason);
   });
}

22、Promise.prototype.catch方法的实现

Promise.prototype.catch = function (onRejected) {
   return this.then(null, onRejected);
}

23、异步加载js脚本的方法有哪些?

a. <script src="../XXX.js" defer></script> // html4

b. <script src="../XXX.js" async></script> // html5

c. 动态创建script标签

d. XHR异步加载JS

deferasync的区别:

a. defer要等到整个页面在内存中正常渲染结束,即DOM结构完全生成,以及其他脚本执行完成,在window.onload之前执行;

b. async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

c. 如果有多个defer脚本,会按照它们在页面出现的顺序加载。

d. 多个async脚本不能保证加载顺序。

示例:

let script = document.createElement('script');
script.src = 'XXX.js';
// 添加到html文件中才会开始下载
document.body.append(script);

let xhr = new XMLHttpRequest();
xhr.open("get","js/xxx.js",true);
xhr.send();

xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    eval(xhr.responseText);
  }
}

24、请实现一个flattenDeep函数,把嵌套的数组扁平化?

【法1】利用Array.prototype.flat

示例:

function flattenDeep(arr, deepLength) {
  return arr.flat(deepLength);
}

console.log(flattenDeep([1, [2, [3, [4]],5]], 3));

function flattenDeep(arr) {
  return arr.flat(Math.pow(2,53) - 1); // JS能表示的最大数字为Math.pow(2, 53) - 1
}

console.log(flattenDeep([1, [2, [3, [4]], 5]]));

【法2】利用reduceconcat

示例:

function flattenDeep(arr){
  return arr.reduce((acc, val) => Array.isArray(val) ?acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
console.log(flattenDeep([1, [2, [3, [4]],5]]));

【法3】使用stack无限反嵌套多层嵌套数组

function flattenDeep(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    // 使用pop从stack中取出并移除值
    const next = stack.pop();
    if (Array.isArray(next)) {
      // 使用push送回内层数组中的元素,不会改动原始输入 original input
      stack.push(...next);
    } else {
      res.push(next);
    }
  }

  // 使用reverse恢复原数组的顺序
  return res.reverse();
}
console.log(flattenDeep([1, [2, [3, [4]],5]]));

25、可迭代对象有什么特点?

a. 具有Symbol.iterator属性,Symbol.iterator()返回的是一个遍历器对象。

b. 可以使用for ... of进行循环。

示例:

let arry = [1, 2, 3, 4];
let iter = arry[Symbol.iterator]();
console.log(iter.next()); //{ value: 1, done: false }
console.log(iter.next()); //{ value: 2, done: false }
console.log(iter.next()); //{ value: 3, done: false }

原生具有Iterator接口的数据结构:Array/Map/Set/String/TypedArray/函数的arguments对象/NodeList对象。

自定义一个可迭代对象:

示例:

let obj = {
  name: "Yvette",
  age: 18,
  job: 'engineer',
  *[Symbol.iterator]() {
    const self = this;
    const keys = Object.keys(self);
    for (let index = 0; index < keys.length; index++) {
      yield self[keys[index]];//yield表达式仅能使用在Generator 函数中
    }
  }
};

26、JSONP原理及简单实现

实现原理:

【Step1:】创建callback方法

【Step2】插入script标签

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

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

源码实现:

function jsonp({url, params, callback}) {
  return new Promise((resolve, reject) => {
    //创建script标签
    let script = document.createElement('script');
    
    //将回调函数挂在 window 上
    window[callback] = function(data) {
      resolve(data);
      //代码执行后,删除插入的script标签
      document.body.removeChild(script);
    }

    //回调函数加在请求地址上
    params = {...params, callback} //wb=b&callback=show
    let arrs = [];
    for(let key in params) {
      arrs.push(`${key}=${params[key]}`);
    }

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

27、实现一个数组去重的方法

【法1】利用ES6新增数据类型Set

示例:

function uniq(arry) {
  return [...new Set(arry)];
}

【法2】利用indexOf

示例:

function uniq(arry) {
  var result = [];
  for (var i = 0; i < arry.length; i++) {
    if (result.indexOf(arry[i]) === -1) {
      result.push(arry[i])
    }
  }

  return result;
}

【法3】利用includes

示例:

function uniq(arry) {
  var result = [];
  for (var i = 0; i < arry.length; i++) {
    if (!result.includes(arry[i])) {
      result.push(arry[i]);
    }
  }

  return result;
}

【法】利用reduce

示例:

function uniq(arry) {
  return arry.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}

【法】利用Map

示例:

function uniq(arry) {
  let map = new Map();
  let result =[];

  for (let i = 0; i < arry.length; i++) {
    if (map.has(arry[i])) {
      map.set(arry[i], true);
    } else {
      map.set(arry[i], false);
      result.push(arry[i]);
    }
  }

  return result;
}

28、清除浮动的方法有哪些?

场景:

<style>
  .inner {
    width: 100px;
    height: 100px;
    float: left;
  }
</style>
<div class='outer'>
  <div class='inner'></div>
  <div class='inner'></div>
  <div class='inner'></div>
</div>

【法1】利用clear属性

在 <div class='outer'> 内创建一个空元素,对其设置 clear: both; 的样式。

【法2】利用 clear 属性 + 伪元素

示例:

.outer:after{
  content: '';
  display: block;
  clear: both;
  visibility: hidden;
  height: 0;
}

注意事项:IE8以上和非IE浏览器才支持:after,如果想要支持IE6、7,需要给outer元素,设置样式 zoom: 1;

【法3】利用 BFC 布局规则

根据BFC的规则,计算BFC的高度时,浮动元素也参与计算。因此清除浮动,只需要触发一个BFC即可。

示例:

.outer {
  overflow: hidden;
}

29、编写一个通用的柯里化函数currying

函数柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

示例:

const currying = (fn, ...args) =>
//参数长度不足时,重新柯里化该函数,等待接受新参数,参数长度满足时,执行函数
args.length < fn.length ? (...arguments) => currying(fn, ...args, ...arguments) : fn(...args);

function sumFn(a, b, c) {
  return a + b + c;
}

var sum = currying(sumFn);
console.log(sum(2)(3)(5));//10
console.log(sum(2, 3, 5));//10
console.log(sum(2)(3, 5));//10
console.log(sum(2, 3)(5));//10

30、构造函数

构造函数和普通函数的区别仅在于调用它们的方式不同,任何函数,只要通过new操作符来调用,那它就可以作为构造函数;任何函数,如果不通过new操作符来调用,那么它就是一个普通函数。

示例:

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

var Yvette = new Person('刘雪飞', 28);
console.log(Yvette.constructor === Person); //true

注意事项:

除了基本数据类型的constructor外(null和undefined无constructor属性),constructor属性是可以被重写的。因此检测对象类型时,instanceof操作符比 contsrutor更可靠一些。

示例:

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

function SuperType() { }
var Yvette = new Person('刘雪飞');
console.log(Yvette.constructor); // [Function: Person]
Yvette.constructor = SuperType;
console.log(Yvette.constructor); // [Function: SuperType]

31、原型

我们创建的每个函数都有prototype属性,这个属性指向函数的原型对象。原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象,可以通过实例的__proto__来访问构造函数的原型对象。

示例:

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

Person.prototype.sayName = function() {
  console.log(this.name);
}

var person1 = new Person('刘雪飞');
var person2 = new Person('前端小哥哥'); 
person1.sayName();
person1.sayName();

实例.__proto__ === 构造函数.prototype

示例:

console.log(Object.prototype.__proto__ === null)  //true
console.log(Object.__proto__ === Function.prototype) //true
console.log(Function.prototype.__proto__ === Object.prototype) //true

32、原型链

假如我们让原型对象等于另一个类型的实例,那么此时原型对象包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个类型的实例,那么上述关系仍然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。

示例:

function SuperType() {
  this.type = 'animal';
}

SuperType.prototype.getType = function() {
  console.log(this.type);
}

function SubType() {}

SubType.prototype = new SuperType();

SubType.prototype.sayHello = function() {
  console.log('hello');
}

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

SimType.prototype = new SubType();

SimType.prototype.sayHi = function() {
  console.log('hi');
}

var instance = new SimType('刘小夕');
instance.getType();

调用 instance.getType() 会调用以下的搜索步骤:

【step1】搜索 instance 实例

【step2】搜索SimType.prototype

【step3】搜索SubType.prototype

【step4】搜索SuperType.prototype,找到了 getType 方法

33、原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

示例:

function SuperType() {
  this.name = 'Yvette';
  this.colors = ['pink', 'blue', 'green'];
}

SuperType.prototype.getName = function () {
  return this.name;
}

function SubType() {
  this.age = 22;
}

SubType.prototype = new SuperType();

SubType.prototype.getAge = function() {
  return this.age;
}

SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); // 'Yvette'
console.log(instance1.colors); // [ 'pink', 'blue', 'green', 'yellow' ]
let instance2 = new SubType();
console.log(instance2.colors); // [ 'pink', 'blue', 'green', 'yellow' ]

缺点:

a. 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。

b. 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。

34、借用构造函数

借用构造函数的基本思想是在子类型的构造函数中调用超类型构造函数。

示例:

function SuperType(name) {
  this.name = name;
  this.colors = ['pink', 'blue', 'green'];
}

function SubType(name) {
  SuperType.call(this, name);
}

let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors); // ['pink', 'blue', 'green', yellow]
let instance2 = new SubType('Jack');
console.log(instance2.colors); // ['pink', 'blue', 'green']

优点:

a. 可以向超类传递参数

b. 解决了原型中包含引用类型值被所有实例共享的问题

缺点:

方法都在构造函数中定义,函数复用无从谈起,另外超类型原型中定义的方法对于子类型而言都是不可见的。

35、组合继承

组合继承的基本思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

示例:

function SuperType(name) {
  this.name = name;
  this.colors = ['pink', 'blue', 'green'];
}

SuperType.prototype.sayName = function () {
  console.log(this.name);
}

function SuberType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;

SuberType.prototype.sayAge = function () {
  console.log(this.age);
}

let instance1 = new SuberType('Yvette',20);
instance1.colors.push('yellow');
console.log(instance1.colors); // [ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); // Yvette
let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName(); // Jack

优点:

a. 可以向超类传递参数

b. 每个实例都有自己的属性

c. 实现了函数复用

缺点:

无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

36、原型式继承

原型式继承的基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

示例:

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

var person = {
  name: 'Yvette',
  hobbies: ['reading', 'photography']
}

var person1 = Object.create(person);
person1.name = 'Jack';
person1.hobbies.push('coding');
var person2 = Object.create(person);
person2.name = 'Echo';
person2.hobbies.push('running');
console.log(person.hobbies); // ['reading', 'photography', 'coding', 'running']
console.log(person1.hobbies); // ['reading', 'photography', 'coding', 'running']

注意事项:在传入一个参数的情况下,Object.create()object()方法的行为相同。

缺点:同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

37、寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

示例:

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

function createAnother(original) {
  var clone = object(original);//通过调用函数创建一个新对象
  clone.sayHi = function () {//以某种方式增强这个对象
    console.log('hi');
  };
  return clone;//返回这个对象
}

var person = {
  name: 'Yvette',
  hobbies: ['reading', 'photography']
};

var person2 = createAnother(person);
person2.sayHi(); // hi

缺点:

a. 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而效率低下。

b. 同原型链实现继承一样,包含引用类型值的属性会被所有实例共享。

38、寄生组合式继承

寄生组合式继承的基本思想是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

示例:

function SuperType(name) {
  this.name = name;
  this.colors = ['pink', 'blue', 'green'];
}

function SuberType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}

function object(o) {
  function F() { }
  F.prototype = o;
  return new F();
}

function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype); //创建对象
  prototype.constructor = subType;//增强对象
  subType.prototype = prototype;//指定对象
}

优点:只调用了一次超类构造函数,效率更高。避免SuberType.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。

39、ES6继承

Class可以通过extends关键字实现继承。

示例:

class SuperType {
  constructor(age) {
    this.age = age;
  }

  getAge() {
    console.log(this.age);
  }
}

class SubType extends SuperType {
  constructor(age, name) {
    super(age); // 调用父类的constructor(x, y)
    this.name = name;
  }

  getName() {
    console.log(this.name);
  }
}

let instance = new SubType(22, '刘小夕');
instance.getAge(); //22

注意事项:

a. 类的数据类型就是函数,类本身就指向构造函数。

b. 类的内部所有定义的方法,都是不可枚举的。

c. constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

d. Class 能像构造函数那样直接调用,会抛出错误。

e. 子类必须在constructor中调用super方法,否则新建实例时会报错。如果没有子类没有定义constructor方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用super 之后,才能使用this关键字,否则报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

示例:

console.log(typeof SuperType);//function
console.log(SuperType === SuperType.prototype.constructor); //true
Object.keys(SuperType.prototype);

class SubType extends SuperType {
  constructor(...args) {
    super(...args);
  }
}

40、实现一个JSON.stringify

JSON.stringify([, replacer [, space])方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space。

JSON.stringify()将值转换成对应的JSON格式:

1】基本数据类型

a. undefined转换之后仍是undefined(类型也是undefined)

b. boolean值转换之后是字符串"false"/"true"

c. number类型(除了NaNInfinity)转换之后是字符串类型的数值

d. symbol转换之后是undefined

e. null转换之后是字符串"null"

f. string转换之后仍是string

g. NaNInfinity转换之后是字符串"null"

2】如果是函数类型

转换之后是undefined

3】如果是对象类型(非函数)

a. 如果有toJSON()方法,那么序列化toJSON()的返回值。

b. 如果是一个数组,属性值中出现了undefined、任意的函数以及symbol,转换成字符串 "null"

c. 如果是RegExp对象,返回{} (类型是 string)

d. 如果是Date对象,返回DatetoJSON字符串值

e. 如果是普通对象,属性值中出现了undefined、任意的函数以及symbol值,忽略;所有以symbol为属性键的属性都会被完全忽略掉。

4】对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。

示例:

function jsonStringify(data) {
  let dataType = typeof data;
  
  if (dataType !== 'object') {
    let result = data;
    if (Number.isNaN(data) || data === Infinity) {
      result = "null";
    } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
      return undefined;
    } else if (dataType === 'string') {
      result = '"' + data + '"';
    }
    
    return String(result);
  } else if (dataType === 'object') {
    if (data === null) {
      return "null";
    } else if (data.toJSON && typeof data.toJSON === 'function') {
      return jsonStringify(data.toJSON());
    } else if (data instanceof Array) {
      let result = [];
      data.forEach((item, index) => {
        if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
          result[index] = "null";
        } else {
          result[index] = jsonStringify(item);
        }
      });

      result = "[" + result + "]";
      return result.replace(/'/g, '"');
    } else {
      let result = [];
      Object.keys(data).forEach((item, index) => {
        if (typeof item !== 'symbol') {
          if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
            result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
          }
        }
      });

      return ("{" + result + "}").replace(/'/g, '"');
    }
  }
}

41、实现一个 JSON.parse

【法1】eval

示例:

var json = '{"name":"小姐姐", "age":20}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之后得到的对象

【法2】new Function

示例:

var json = '{"name":"小姐姐", "age":20}';
var obj = (new Function('return ' + json))();

42、实现一个观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。

示例:

function Hunter(name, level){
  this.name = name;
  this.level = level;
  this.list = [];
}

Hunter.prototype.publish = function(money){
  console.log(this.level + '猎人' + this.name + '寻求帮助');
  this.list.forEach(function(item, index){
    item(money);
  })
}

Hunter.prototype.subscribe = function(targrt, fn){
  console.log(this.level + '猎人' + this.name + '订阅了' + targrt.name)
  targrt.list.push(fn)
} 

let hunterMing = new Hunter('小明', '黄金')
let hunterJin = new Hunter('小金', '白银')
let hunterZhang = new Hunter('小张', '黄金')
let hunterPeter = new Hunter('Peter', '青铜')

hunterMing.subscribe(hunterPeter, function(money){
  console.log('小明表示:' + (money > 200 ? '' : '暂时很忙,不能') + '给予帮助')
});

hunterJin.subscribe(hunterPeter, function(){
  console.log('小金表示:给予帮助')
});

hunterZhang.subscribe(hunterPeter, function(){
  console.log('小张表示:给予帮助')
}); 

hunterPeter.publish(198);

43、使用 CSS 让一个元素水平垂直居中

父元素 .container

子元素 .box

【法1】利用flex布局

示例:

.container {
  display: flex;
  align-items: center;
  justify-content: center;
}

【法2】子元素是单行文本

示例:

.container {
  height: 100px;
  line-height: 100px;
  text-align: center;
}

【法3】利用absolute + transform

示例:

.container {
  position: relative;
}
.box {
  position:
  absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

【法4】利用grid布局

示例:

.container {
  display: grid;
}
.box {
  justify-self: center;
  align-self: center;
}

【法5】利用绝对定位和margin:auto

示例:

.container {
  position: relative;
}
.box {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

44、ES6模块和CommonJS模块有哪些差异?

1CommonJS模块是运行时加载,ES6模块是编译时输出接口。

a. ES6模块在编译时,就能确定模块的依赖关系以及输入和输出的变量,ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

b. CommonJS加载的是一个对象,该对象只有在脚本运行完才会生成。

2CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用。

a. CommonJS输出的是一个值的拷贝(注意基本数据类型/复杂数据类型)

b. ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS模块输出的是值的拷贝:

1】模块输出的值是基本数据类型,模块内部的变化就影响不到这个值。

示例:

//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; },300);
module.exports = name;

//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name),500); //William

2】模块输出的值是复杂数据类型

模块输出的是对象,属性值是简单数据类型时:

示例:

//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; },300);
module.exports = { name };

//index.js
const { name } = require('./name');
console.log(name); //William
//name是一个原始类型的值,会被缓存。
setTimeout(() => console.log(name),500); //William

模块输出的是对象,属性值是复杂数据类型时:

示例:

//name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => { 
  name = 'Yvette';
  hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };

//index.js
const { name, hobbies } = require('./name');
console.log(name); // William
console.log(hobbies); // ['coding']

setTimeout(() => {
  console.log(name);//William
  // 复杂数据类型,拷贝的栈内存中存的地址
  console.log(hobbies);//['coding', 'reading']
}, 500);

ES6模块遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。export default可以理解为将变量赋值给default,最后导出default。基础类型变量name,赋值给default之后,只读引用与default关联,此时原变量name的任何修改都与default无关;复杂数据类型变量hobbies,赋值给default之后,只读引用与default关联,default和 hobbies中存储的是同一个对象的堆内存地址,当这个对象的值发生改变时,此时 default 的值也会发生变化。

示例:

//name.js
let name = 'William';
setTimeout(() => { 
  name = 'Yvette';
  hobbies.push('writing'); 
}, 300);
export { name };
export var hobbies = ['coding'];

//index.js
import { name, hobbies } from './name';
console.log(name, hobbies); // William, ["coding"]
//name和hobbie都会被模块内部的变化所影响
setTimeout(() => {
  console.log(name, hobbies); // Yvette ["coding","writing"]
}, 500); // Yvette

3ES6模块自动采用严格模式,无论模块头部是否写了"use strict"

4require可以做动态加载,import语句做不到,import语句必须位于顶层作用域中。

5ES6模块的输入变量是只读的,不能对其进行重新赋值。

6)当使用require命令加载某个模块时,就会运行整个模块的代码。

7)当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。