前端面试基础题:javascript获取DOM元素、鼠标、键盘事件...
✨ 获取DOM元素
document.getElementsByTagName('tagName')
document.getElementsByClassName('class')
document.getElementById('id')
document.querySelector('selector')
document.querySelectorAll('selector')
document.body  // 返回body元素对象
document.documentElement  // 返回html元素对象
✨ 鼠标事件、键盘事件
| 鼠标事件 | 触发条件 | 
|---|---|
| onclick | 鼠标左键点击 | 
| onmouseover | 鼠标经过触发(有冒泡效果) | 
| onmouseout | 鼠标离开触发(有冒泡效果) | 
| onfocus | 获得鼠标焦点触发(无冒泡效果) | 
| onblur | 失去鼠标焦点触发(无冒泡效果) | 
| onmousemove | 鼠标移动触发 | 
| onmouseup | 鼠标弹起触发 | 
| onmousedown | 鼠标按下触发 | 
| onmouseleave | 鼠标离开触发(无冒泡效果) | 
| onmouseenter | 鼠标经过触发(无冒泡效果) | 
| 键盘事件 | 触发条件 | 
|---|---|
| onkeyup | 某个键盘按键松开时触发 | 
| onkeydown | 某个键盘按下时触发 | 
| onkeypress | 某个键盘按下时触发,不识别功能键 | 
| 鼠标事件对象 | 说明 | 
|---|---|
| e.clientX / e.clientY | 返回鼠标相对于浏览器窗口可视区的X/Y坐标 | 
| e.pageX / e.pageY | 返回鼠标相对于文档页面的X/Y坐标 | 
| e.screenX / e.screenY | 返回鼠标相对于电脑屏幕的X/Y坐标 | 
| 键盘事件对象 | 说明 | 
|---|---|
| e.keyCode | 返回该键的ASCII码 | 
change事件在input失去焦点才会考虑触发,它的缺点是无法实时响应,与blur事件有着相似的功能,但与blur事件不同的是,change事件在输入框的值未改变时并不会触发,当输入框的值和上一次的值不同,并且输入框失去焦点,就会触发change事件。
✨ 操作元素
| 方法 | 作用 | 
|---|---|
| element.innerText | 改变元素内容,不识别html标签,去除空格和换行 | 
| element.innerHTML | 改变元素内容,包括html标签,保留空格和换行 | 
| element.style | 行内样式操作 | 
| element.className | 类名样式操作 | 
| element.属性 | 获取内置属性值 | 
| element.getAttribute('属性') | 获取自定义属性值 | 
| element.属性 = 值 | 设置内置属性值 | 
| element.setAttribute('属性', 值) | 设置自定义属性值 | 
| element.removeAtribute('属性') | 移除自定义属性 | 
✨ 节点操作
| 方法 | 作用 | 
|---|---|
| node.parentNode | 获取某节点的父节点 | 
| parentNode.childNodes | 返回指定节点的子节点 | 
| parentNode.children | 返回指定节点的子元素节点 | 
| parentNode.firstChild | 返回指定节点的第一个子节点 | 
| parentNode.lastChild | 返回指定节点的最后一个子节点 | 
| parentNode.firstElementChild | 返回指定节点的第一个子元素节点 | 
| parentNode.lastElementChild | 返回指定节点的最后一个子元素节点 | 
| node.nextSibling | 返回当前节点的下一个兄弟节点 | 
| node.previousSibling | 返回当前节点的上一个兄弟节点 | 
| node.nextElementSibling | 返回当前节点的下一个兄弟元素节点 | 
| node.previousElementSibling | 返回当前节点的上一个兄弟元素节点 | 
| document.createElement('tagName') | 创建节点 | 
| node.appendChild(child) | 添加一个节点到指定父节点子节点列表末尾 | 
| node.insertBefore(child, 指定元素) | 添加一个节点到父节点的指定子节点之前 | 
| node.removeChild(child) | 删除子节点,并返回删除的节点 | 
| node.cloneNode() | 克隆节点,false浅拷贝,不包括子节点,true深拷贝 | 
✨ 注册和删除事件
- 传统方法
 
// 注册
<button onclick="alert('Hi')"></button>
btn.onclick = function () {}
// 删除
eventTarget.onclick = null;
- 方法监听
 
// 注册
eventTarget.addEventListener(eventNameWithoutOn, listener[, useCapture]);
// eventNameWithoutOn:事件类型字符串,不带on
// listener:事件处理函数
// useCapture:默认false,是否使用事件捕获
// 删除
eventTarget.removeEventListener(eventNameWithoutOn, listener[, useCapture]);
- 传统注册方法,同一个事件只能设置一个处理函数,后面覆盖前面。方法监听注册方式可以注册多个监听器,按注册顺序执行
 - 传统注册方式可以通过
return false阻止默认行为。只能得到冒泡阶段 eventTarget可以同时绑定两个事件,既有冒泡事件,又有捕获事件。
✨ DOM事件流
事件流描述的是从页面中接收事件的顺序。分为三个阶段:捕获阶段、当前目标阶段、冒泡阶段。
- 事件捕获:从DOM的根元素(html)开始去执行对应的事件。
 - 事件冒泡:当一个元素的事件被触发时,同样的事件会在该元素的所有祖先元素中依次触发。(默认存在)
 
✨ 事件对象
| 事件对象属性与方法 | 说明 | 
|---|---|
| e.target | 返回事件触发的对象 | 
| e.type | 返回事件的类型 | 
| e.preventDefault() | 阻止默认事件(如链接跳转) | 
| e.stopPropagation() | 阻止冒泡 | 
- e.target和e.currentTarget的区别:
e.currentTarget是事件绑定的元素,是这个函数的调用者,绑定这个事件的元素;e.target是事件触发的元素。在事件冒泡情况下,e.target始终为触发事件的元素,e.currentTarget是每一层绑定了冒泡事件的元素。 - 阻止默认事件的方法:
 
e.preventDefault()
return false  // 不适用于直接用onclick绑定的事件
e.returnValue=false  //IE678 阻止默认行为
- 阻止事件冒泡的方法:
 
e.stopPropagation()
e.cancelBubble=true  //IE678 阻止冒泡
✨ 事件委托
不是为每个子节点单独设置事件监听器,而是将事件监听器设置在父结点上,利用冒泡原理影响每个子节点。
✨ DOM和BOM
- DOM
- 文档对象模型
 - DOM就是将[文档]作为一个[对象]来看待
 - DOM的顶级对象是
document - DOM主要是学习的是操作页面元素
 - DOM是W3C标准规范
 
 - BOM
- 浏览器对象模型
 - BOM把[浏览器]当作一个[对象]来看待
 - BOM的顶级对象是
window - BOM学习的是浏览器窗口交互的一些对象
 - BOM是浏览器厂商在各自浏览器上定义的,兼容性差
 
 
✨ window对象的常见事件
| 事件 | 说明 | 
|---|---|
| load | 窗口加载事件 | 
| DOMContentLoaded | DOM加载完成事件 | 
| resize | 调整窗口大小事件 | 
✨ load、$(document).ready、DOMContentLoaded的区别?
$(document).ready、DOMContentLoaded:DOM树构建完毕,但还没有请求静态资源
load:静态资源请求完毕
window.addEventListener('load', function() {
    console.log('load执行');
});
window.addEventListener('DOMContentLoaded', function() {
    console.log('DOMContentLoaded执行');
})
✨ 定时器
let timeoutId = window.setTimeout(调用函数, 延迟毫秒数);
window.clearTimeout(timeoutId);
let intervalId = window.setInterval(调用函数, 间隔毫秒数);
window.clearInterval(intervalId);
setTimeout和setInterval在执行到当前位置时,就已经开始计时了。等到时间到了再进入宏任务队列。setTimeout函数中内容被放入宏任务队列的顺序与代码书写的顺序无关,而是与计时器时间设定的长短有关,时间越长则越后加入队列,也越后被读取执行。
✨ JS执行队列
JS是单线程的,JS中同步任务在主线程上执行,形成一个执行栈,JS的异步是通过回调函数实现,异步任务相关回调函数被添加到任务队列中。
- Event Loop(事件循环):所有同步任务在主线程上执行,形成一个执行栈。主线程之外还有任务队列,当异步任务执行有结果的时候就会在任务队列放置一个事件。当执行栈中的同步任务执行完毕,就会读取任务队列中的事件,将其对应的异步任务放入执行栈执行,这个不断循环往复的过程,就称为事件循环,也就是Event Loop。
 
JS的执行机制:
- 先执行执行栈中的同步任务;
 - 异步任务放入任务队列;
 - 一旦执行栈中的所有同步任务执行完毕,系统就会按照次序读取任务队列中的异步任务。于是被读取的异步任务结束等待状态,进入执行栈,开始执行。、
 
JS如何实现多线程:
在Web Workers API的帮助下,我们可以在浏览器中实现多线程。Web Workers API 只能在浏览器环境中使用,Node.js 不支持 Web Workers API。
Web Workers API允许我们在后台运行一个脚本,而不影响页面的性能和用户界面。这个脚本运行在与主线程不同的线程中,可以执行一些计算密集型的操作。在主线程和工作线程之间通信可以通过消息传递来完成。
使用Web Workers API创建一个工作线程:
// 创建一个工作线程
const worker = new Worker('worker.js');
// 向工作线程发送消息
worker.postMessage({ type: 'add', data: [1, 2]});
// 监听从工作线程返回的消息
worker.onmessage = function(event) {
    console.log('Received message from worker:', event.data);
}
// worker.js实现加法操作
// 监听从主线程发送的消息
self.onmessage = function(event) {
    if (event.data.type === 'add') {
        const sum = event.data.data.reduce((a, b) => a + b);
        // 向主线程发送消息
        self.postMessage(sum);
    }
};
✨ location对象
用于获取或设置窗体的URL,并且可以用于解析URL。
| location对象的属性和方法 | 返回值 | 
|---|---|
| location.href | 获取或设置整个URL | 
| location.host | 返回主机(域名) | 
| location.port | 返回端口号(未写,返回空字符串) | 
| location.pathname | 返回路径 | 
| location.search | 返回参数 | 
| location.hash | 返回片段 #后面内容(锚点) | 
| location.assign() | 和href一样,可以跳转页面(也称为重定向) | 
| location.replace() | 替换当前页面,不记录历史 | 
| location.reload() | 重新加载页面,参数为true会强制刷新 | 
✨ navigator对象
包含有关浏览器的信息,最常用的是userAgent,该属性返回由客户机发送服务器的user-agent头部的信息。
✨ history对象
和浏览器历史记录进行交互。
| history对象的属性和方法 | 返回值 | 
|---|---|
| history.back() | 后退功能 | 
| history.forward() | 前进功能 | 
| history.go(参数) | 前进后退功能,参数=1前进一个页面,参数=-1后退一个页面 | 
✨ 元素offset、client、scroll系列

✨ 总结获取viewport和element尺寸和位置的方法

✨ 本地存储(sessionStorage、localStorage)

- 数据存储在用户浏览器中。
 - 只能存储字符串,可以将对象编码为JSON格式存储。(
JSON.stringify()/JSON.parse()) 
✨ 类和类的继承(ES6)
// 父类
class Father {
    constructor (surname) {
        this.surname = surname;
    }
    say () {
        return '我是爸爸';
    }
}
// 子类
class Son extends Father {
    constructor (surname, firstname) {
        super(surname);
        this.firstname = firstname;
    }
    say () {
        return super.say() + '的儿子';
    }
}
- 子类构造函数中使用
super必须放到this前面。 - ES6中没有类提升,所以必须先定义再使用。
 constructor中的this指向实例对象,方法里面的this指向这个方法的调用者。
✨ 构造函数和原型
构造函数原型prototype(显式原型)
每一个构造函数都会有一个prototype属性,指向另一个对象,这个prototype就是一个对象,这个对象所有的属性和方法都会被构造函数所拥有,我们可以将那些不变的方法直接定义在prototype对象上,这样所有对象实例能够共享这些方法。
对象原型__proto__(隐式原型)
对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__。
constructor构造函数
对象原型__proto__和构造函数原型对象prototype里面有一个属性constructor属性,指回构造函数本身,constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。

如果原型对象采用对象形式重新赋值,这样会覆盖构造函数原型对象原来的内容,修改后的原型对象constructor就不再指向当前构造函数了,此时需要在修改后的原型对象中添加一个constructor指向原来的构造函数。
构造函数+原型对象实现组合继承(ES5)
// 使用构造函数继承父类属性
function Person (name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
function Student (name, age, sex, score) {
    Person.call(this, name, age, sex);
    this.score = score;
}
// 使用原型对象继承父类方法
Person.prototype.say = function() {
    console.log(`我叫${this.name}`);
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
原型链

✨ 函数this指向(call、apply、bind)
- 函数内的this指向
 
| 调用方式 | this指向 | 
|---|---|
| 普通函数调用 | window | 
| 构造函数调用 | 实例对象,原型对象里面的方法也指向实例对象 | 
| 对象方法调用 | 该方法所属对象 | 
| 事件绑定方法 | 绑定事件对象 | 
| 定时器函数 | window | 
| 立即执行函数 | window | 
- 修改函数内部this指向的函数
 
fun.call(thisArg, arg1, arg2, …)
fun.apply(thisArg, [arg1, arg2, …])
fun.bind(thisArg, arg1, arg2, …)
// thisArg:在fun函数运行时指定的this值
// arg1, arg2, ...:传递的参数
- call、apply、bind的异同点
相同点:都可以改变函数内部this指向
不同点:call和apply会调用函数,bind不会调用函数,返回指定this和初始化参数改造的原函数的拷贝;call和bind传递参数以arg1,arg2,…的形式,apply传递参数以[arg1, arg2, …]数组的形式。 
手写call、apply、bind
Function.prototype.myCall = function (context, ...args) {
    context = (typeof context === 'object'? context: window);
    const key = Symbol();  // 防止覆盖掉原有属性
    context[key] = this;  // this是需要执行的方法
    const result = context[key](...args);  // 方法执行
    delete context[key];
    return result;
}
Function.prototype.myApply = function (context, args) {
    context = (typeof context === 'object'? context: window);
    const key = Symbol();
    context[key] = this;
    const result = context[key](...args);
    delete context[key];
    return result;
}
Function.prototype.myBind = function (context, ...args) {
    context = (typeof context === 'object'? context: window);
    return (...args) => {
        this.call(context, ...args);
    }
}
✨ 箭头函数和普通函数的区别
- 箭头函数的
this指向的是函数定义位置的上下文this,而不是调用它的对象 - 箭头函数不会进行函数提升
 - 箭头函数没有
arguments对象 - 没有
yield属性,不能作为生成器Generator使用 - 不能
new - 没有
prototype,new关键字内部需要把新对象的__proto__指向函数的prototype 
✨ this指向问题
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
绑定规则
- 默认绑定:最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
 - 隐式绑定:考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的
this绑定到这个上下文对象。
隐式丢失一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。 
function foo () {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo;  // 函数别名
var a = 'oops, global'  // a是全局对象的属性
bar();  // 'oops, global'
3. 显式绑定:使用call()或apply()函数,在某个对象上强制调用函数。它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通常被称为“装箱”。但显式绑定仍然不能解决之前提出的丢失绑定的问题。解决方法:
- 
- 硬绑定:创建一个函数,内部实现函数的
this绑定,所以每次执行这个函数,都会在绑定的this上执行绑定的函数。(硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind) - API调用的上下文:第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和
bind(..)一样,确保你的回调函数使用指定的this。(例如:forEach) 
 - 硬绑定:创建一个函数,内部实现函数的
 
4. new绑定:使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。
优先级
new > 显式绑定 > 隐式绑定 > 默认绑定 。
判断this:
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
- 函数是否在
new中调用(new绑定)?如果是的话,·this绑定的是新创建的对象。
var bar = new foo(); - 函数是否通过
call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2); - 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,
this绑定的是那个上下文对象。
var bar = obj1.foo(); - 如果都不是的话,使用默认绑定(独立函数调用)。如果在严格模式下,就绑定到
undefined,否则绑定到全局对象。
var bar = foo(); 
特殊情况
- 如果你把
null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。在严格模式中,null就是null,undefined就是undefined。 - 箭头函数不使用
this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。箭头函数的this是在创建它时外层this的指向。箭头函数中this不会被修改。 
var name = "window";
var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};
function sayName() {
  var sss = person.sayName;
  sss();  // window
  person.sayName();  // person
  (person.sayName)();   // person
  (b = person.sayName)();   // window
}
sayName();
var name = 'window'
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person2 = { name: 'person2' }
person1.foo1();   // person1
person1.foo1.call(person2);  // person2
person1.foo2();  // window
person1.foo2.call(person2);  // window
person1.foo3()();  // window
person1.foo3.call(person2)();  // window
person1.foo3().call(person2);  // person2
person1.foo4()();  // person1
person1.foo4.call(person2)();  // person2
person1.foo4().call(person2);  // person1
var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1()  // person1
person1.foo1.call(person2)  // person2
person1.foo2()  // person1
person1.foo2.call(person2)  // person1
person1.foo3()()  // window
person1.foo3.call(person2)()  // window
person1.foo3().call(person2)  // person2
person1.foo4()()  // person1
person1.foo4.call(person2)()  // person2
person1.foo4().call(person2)  // person1
var name = 'window'
function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()()  // window
person1.obj.foo1.call(person2)()  // window
person1.obj.foo1().call(person2)  // person2
person1.obj.foo2()()  // obj
person1.obj.foo2.call(person2)()  // person2
person1.obj.foo2().call(person2)  // obj
✨ 作用域和作用域链
规定变量和函数的可使用范围称作作用域。
每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链。
作用域&变量提升&闭包相关题目
- 立即执行函数模仿块级作用域,匿名函数中定义的任何变量,都会在执行结束时被销毁。
 - 注意变量、函数提升:变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值;函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用。
 - 函数提升会优先于变量提升,而且不会被同名的变量覆盖,但是,如果这个同名变量已经赋值了,那函数变量就会被覆盖。当二者同时存在时,会先指向函数声明。
 - JS中变量的作用域链与定义时的环境有关,与执行时无关。执行环境只会改变this、传递的参数、全局变量等
 
(function(){
   var x = y = 1;
})();
var z;
console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined
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() {
    var temp = 10;
    function b() {
        console.log(temp); // 10
    }
    b();
}
a();
// -----------------------------
function a() {
    var temp = 10;
    b();
}
function b() {
    console.log(temp); // 报错 Uncaught ReferenceError: temp is not defined
}
a();
✨ 执行上下文
执行上下文类型
- 全局执行上下文:任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的
window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。 - 函数执行上下文:当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
 - eval函数执行上下文:执行在
eval函数中的代码会有属于他自己的执行上下文,不常使用。 
执行上下文栈
- JavaScript引擎使用执行上下文栈来管理执行上下文
 - 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
 
✨ 严格模式(ES5)
在脚本文件或函数开头添加 ”use strict”; 语句。 严格模式的要求:
- 变量规定:不允许变量没有用
var声明就赋值; - this指向问题:非严格模式下全局作用域
this指向window,严格模式下this指向undefined; - 函数变化:不能有重名的参数,函数必须声明在顶层。
 
✨ 高阶函数
高阶函数时对其他函数进行操作的函数,接收函数作为参数或将函数作为返回值输出。
✨ 闭包
闭包是指有权访问另一个函数作用域中变量的函数。变量所在的函数是闭包函数。
作用:延长了外部函数内的变量的生命周期,能够从外部间接访问函数内部变量。但是这些变量的值始终保存在内存中,不回收,容易造成内存泄漏。
function fn () {  // fn是闭包函数
    var num = 10;
    return function () {
        console.log(num);
    }
}
var f = fn();
f();
应用举例:
- 实现节流的时候,通过闭包保存节流阀变量
 
function throttle(fn, delay) {
    let flag = false;
    return function () {
        if (flag) return;
        flag = true;
        setTimeout(() => {
            fn.apply(this, arguments);
            flag = false;
        }, delay);
    };
}
2. 函数柯里化实现:用闭包保存传入的部分参数
const add = (a, b, c) => a + b + c;
const currying = (fn, ...args) => {
    let allArgs = [...args];
    const num = fn.length;
    const res = (...args2) => {
        allArgs = [...allArgs, ...args2]
        if (allArgs.length === num) {
            return fn(...allArgs);
        } else {
            return res;
        }
    }
    return res;
}
const a = currying(add, 1);
console.log(a(2)(3));
✨ JS垃圾回收机制
垃圾回收(Garbage Collection, GC):程序工作过程中会产生很多垃圾(不再用的内存空间),GC就是负责回收垃圾的,工作在JS引擎内部。
为什么需要进行垃圾回收:程序的运行需要内存,只要程序提出要求,就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则内存的占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。
垃圾回收策略
每隔一段时间,JS的“垃圾收集器”都会对变量进行“巡逻”,当一个变量不被需要了以后,它就会把这个变量所占用的内存空间释放,这个过程叫做“垃圾回收”。
JS的垃圾回收算法分为:引用计数法和标记清除法。
- 引用计数法
思想:将"对象是否不再需要"简化定义为"对象有没有其他对象引用到它",如果没有引用指向该对象,对象将被垃圾回收机制回收。
策略:跟踪记录每个变量值被使用的次数,当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为1。如果同一个值又被赋给另一个变量,那么引用数加1,如果该变量的值被其他的值覆盖了,则引用次数减1。当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行时清理掉引用次数为0的值占用的内存。
优点:清晰,在变成垃圾的那一刻就会被回收。标记清除法需要每隔一段时间进行一次,另外需要遍历堆中的活动和非活动对象来清除,而引用计数则只需要在引用时计数就可以了。
缺点:计数器需要占用很大的位置,无法解决互相引用的问题。 - 标记清除法(最常用)
采用的判断标准是看这个对象是否可抵达,它主要分为两个阶段,标记阶段和清除阶段:
标记阶段:垃圾收集器会从根对象(Window对象)出发,扫描所有可以触及的对象,这就是所谓的可抵达;
清除阶段:在扫描的同时,根对象无法触及(不可抵达)的对象,就是被认为不被需要的对象,就会被当成垃圾清除。
优点:实现简单,只有打与不打标记两种情况,用一位二进制位就可以为其标记。
缺点:清除之后,剩余对象内存位置不变,导致空闲内存空间是不连续的,出现内部碎片。采用内存分配策略进而导致分配速度慢。 
V8对GC的优化
- 分代式垃圾回收
V8将堆内存划分为新生代和老生代两区域,分别存储新、小、存活时间短的对象和老、大、存活时间长的对象,并采用不同的策略管理垃圾回收。 

新生代垃圾回收策略:
- 
- 将堆内存一分为二,一个是处于使用状态的空间,称之为"使用区",一个是处于闲置状态的空间,称之为"空闲区"。
 - 新加入的对象都会被放到使用区,当使用区快被写满时,执行一次垃圾清理操作。
 - 当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象进行标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,将非活动对象占用的空间清理掉,最后进行角色互换,原来的使用区变为空闲区,原来的空闲区变为使用区。
 - 如果对一个对象复制多次后依然存活,将被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理。
 - 如果复制一个对象到空闲区时,对空闲区的占用超过了25%,这个对象会被直接移动到老生代中。
 
 
老生代垃圾回收策略:采用标记清除法,并采用标记整理方法优化空间。
2. 并行回收

进行垃圾回收时,会阻塞JS脚本的执行,等待垃圾回收完毕后再恢复脚本执行,这种行为称为全停顿。V8引入了并行回收机制,即垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作。
3. 增量标记与惰性清理
增量标记:为了减少全停顿的时间,从全停顿标记切换到增量标记,即将一次完整的GC标记分次执行,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮GC标记。

惰性清理:增量标记完成后,如果当前的可用内存足以快速执行代码,其实是没必要立即清理内存的,可以将清理过程稍微延迟一下,让JS脚本先执行,也无需一次性清理完所有非活动对象的内存,可以按需逐一清理直到所有非活动对象的内存都清理完毕,后面再接着执行增量标记。
4. 并发回收
并行回收依然会阻塞主线程,并发回收指的是主线程在执行JS的过程中,辅助线程在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起。
✨解构赋值、剩余参数(ES6)
// 解构赋值
let [a, b, c] = [1, 2, 3];
let person = { name: “zhangsan”, age: 20 };
let {name: myName, age: myAge} = person;
// 剩余参数
function sum (first, …args) {
	console.log(first);  // 10
  	console.log(args);  // [20, 30]
}
sum(10, 20, 30);
✨ 图片懒加载
如果打开网页时让所有图片一次性加载完成,需要处理很多次网络请求,等待加载时间比较长,用户体验感很差。有一种常用的解决方式是:随着页面滚动动态加载,即图片的懒加载。
原理
- 初始化的时候,可以设置图片的
src是某一个小型图片。例如一张1px*1px的透明图片。由于所有图片都使用这一张图片,只会发送一次请求,不会增加性能负担。 - 将图片的真实路径绑定给一个自定义属性,例如 
data-url。注意:页面的img元素,如果没有src属性,浏览器就不会发出请求去下载图片<img data-url="xxx" src="1px.gif" width="100" height="100"/>。 - 定义滚动事件,判断元素进入视口,则将
src替换为真正的url地址。利用js提取data-url的真实图片地址赋值给src属性。 
实现方法
图片懒加载的关键在于获取元素的位置,并判断其是否出现在视口,有以下三种方式:
- 滚动监听+scrollTop+offsetTop+innerHeight
 
document.documentElement.scrollTop/window.pageYOffset:指网页元素被滚动条卷去的部分。img.offsetTop:元素相对父元素的位置window.innerHeight/document.documentElement.clientHeight:当前浏览器窗口的大小 当document.documentElement.scrollTop + window.innerHeight > img.offsetTop,即图片在视口内,否则图片在可视区域外。

2. 滚动监听+getBoundingClientRect() Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
API返回一个对象,即rectObject为一个对象,其包含以下属性
- 
rectObject.top:元素上边到视窗上边的距离;rectObject.right:元素右边到视窗左边的距离;rectObject.bottom:元素下边到视窗上边的距离;rectObject.left:元素左边到视窗左边的距离;rectObject.width:元素自身的宽度rectObject.height:元素自身的高度 故当rectObject.top的值处于0~视口高度,则元素处于可视区:rectObject.top > 0 && rectObject.top < window.innerHeight
 

3. IntersectionObserver (callback) IntersectionObserver()针对元素的可见时间进行监听。目标元素的可见性变化时,就会调用观察器的回调函数callback。一般会触发两次:(1) 目标元素刚刚进入视口(开始可见);(2) 完全离开视口(开始不可见)。callback函数的参数是一个数组,每个成员都是一个IntersectionObserverEntry对象。可以通过判断这个对象的intersectionRatio属性是否处于(0,1)来判断元素的可见性。
✨ 轮播图
- 第一张图片前放置最后一张图片,最后一张图片前放置第一张图片,以实现图片的无缝轮播。
 - 存放图片的ul宽度设置为所有图片长度之和,ul中的li左浮动。
 - 实现图片移动的两种方式:
translate、position - 鼠标移动到轮播图上停止自动播放(清除自动播放的定时器),移出轮播图区域就开始自动播放(启动自动播放定时器)。
 - 如果走到了最后那张复制的图片,此时需要快速回到第一张图片的位置;如果走到了最前面复制的那张图片,此时需要快速回到最后一张图片的位置。
 - 底部小圆圈利用排他思想:先将所有小圆点current类名去掉,再为当前小圆点设置current类名。
 
<div class="focus">
    <!-- 左右箭头 -->
    <a href="javascript:;" class="arrow-l"><</a>
    <a href="javascript:;" class="arrow-r">></a>
    <!-- 图片 -->
    <ul>
        <li><a href="#"><img src="images/focus.jpg" alt=""></a></li>
        <li><a href="#"><img src="images/focus1.jpg" alt=""></a></li>
        <li><a href="#"><img src="images/focus2.jpg" alt=""></a></li>
        <li><a href="#"><img src="images/focus3.jpg" alt=""></a></li>
    </ul>
    <!-- 小圆点 -->
    <ol class="circle"></ol>
</div>
* {
    margin: 0;
    padding: 0;
}
a {
    text-decoration: none;
}
li {
    list-style: none;
}
.focus {
    position: relative;
    margin: 100px auto;
    width: 721px;
    height: 455px;
    overflow: hidden;
}
.focus ul {
    position: absolute;
    top: 0;
    left: 0;
    width: 600%;
}
.focus ul li {
    float: left;
}
.arrow-l,
.arrow-r {
    display: none;
    position: absolute;
    top: 50%;
    margin-top: -20px;
    height: 40px;
    width: 24px;
    background: rgba(0, 0, 0, .3);
    text-align: center;
    line-height: 40px;
    color: #fff;
    font-family: 'icomoon';
    font-size: 18px;
    z-index: 2;
}
.arrow-r {
    right: 0;
}
.circle {
    position: absolute;
    bottom: 10px;
    left: 50px;
}
.circle li {
    float: left;
    width: 8px;
    height: 8px;
    border: 2px solid rgba(255, 255, 255, 0.5);
    margin: 0 3px;
    border-radius: 50%;
    cursor: pointer;
}
.current {
    background-color: #fff;
}
// 封装动画函数
function animate(obj, target, callback) {
    // 先清除以前的定时器,只保留当前的一个定时器执行
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var step = (target - obj.offsetLeft) / 10;
        step = step > 0 ? Math.ceil(step) : Math.floor(step);
        if (obj.offsetLeft == target) {
            clearInterval(obj.timer);
            callback & callback();
        }
        obj.style.left = obj.offsetLeft + step + 'px';
    }, 15);
}
window.addEventListener('load', function () {
    // 获取元素
    var arrow_l = document.querySelector('.arrow-l');
    var arrow_r = document.querySelector('.arrow-r');
    var focus = document.querySelector('.focus');
    var focusWidth = focus.offsetWidth;
    // 鼠标经过focus就显示左右按钮,并停止自动播放
    focus.addEventListener('mouseenter', function () {
        arrow_l.style.display = 'block';
        arrow_r.style.display = 'block';
        clearInterval(timer);
        timer = null;
    });
    // 鼠标移出focus就隐藏左右按钮,并启动自动播放
    focus.addEventListener('mouseleave', function () {
        arrow_l.style.display = 'none';
        arrow_r.style.display = 'none';
        timer = setInterval(function () {
            arrow_r.click();
        }, 2000);
    });
    // 动态生成小圆点
    var ul = focus.querySelector('ul');
    var ol = focus.querySelector('ol');
    for (var i = 0; i < ul.children.length; i++) {
        var li = document.createElement('li');  // 创建一个小圆点
        li.setAttribute('index', i);  // 自定义属性记录小圆点的索引号
        ol.appendChild(li);
        // 小圆点点击事件
        li.addEventListener('click', function () {
            // 先清除所有小圆点current类名
            for (var i = 0; i < ol.children.length; i++) {
                ol.children[i].className = '';
            }
            // 再给当前小圆点设置current类名
            this.className = 'current';
            var index = this.getAttribute('index');
            num = index;
            circle = index;
            // 使用封装的动画函数移动到目标位置
            animate(ul, -index * focusWidth)
        });
    }
    ol.children[0].className = 'current';
    // 克隆包含第一张图片的li放到ul的最后面
    var first = ul.children[0].cloneNode(true);
    ul.appendChild(first);
    var num = 0;
    var circle = 0;
    var flag = true;  // 节流阀
    // 右按钮点击事件
    arrow_r.addEventListener('click', function () {
        if (flag) {
            flag = false;
            // 走到最后一张复制的图片时,快速回到第一张的位置
            if (num == ul.children.length - 1) {
                ul.style.left = 0;
                num = 0;
            }
            num++;
            animate(ul, -num * focusWidth, function () {
                flag = true;
            });
            circle++;
            if (circle == ol.children.length) {
                circle = 0;
            }
            circleChange();
        }
    });
    // 左按钮点击事件
    arrow_l.addEventListener('click', function () {
        if (flag) {
            flag = false;
            // 走到第一张图片时,快速回到最后一张复制的图片
            if (num == 0) {
                num = ul.children.length - 1;
                ul.style.left = -num * focusWidth + 'px';
            }
            num--;
            animate(ul, -num * focusWidth, function () {
                flag = true;
            });
            circle--;
            circle = circle < 0 ? ol.children.length - 1 : circle;
            circleChange();
        }
    });
    // 设置小圆点current类名
    function circleChange() {
        for (var i = 0; i < ol.children.length; i++) {
            ol.children[i].className = '';
        }
        ol.children[circle].className = 'current';
    }
    // 自动播放轮播图
    var timer = setInterval(function () {
        arrow_r.click();
    }, 2000);
});






