JS手写相关
一.call、apply、bind函数
1.三个函数有什么区别?
将函数绑定到上下文中,用来改变函数中this指向的。
- fun.call(thisArg,arg1,arg2...)
- fun.apply(thisArg,[arg1,arg2...])
- fun.bind(thisArg,arg1,arg2...)
参数:
thisArg(可选):
- fun的this指向thisArg对象
- 非严格模式下:thisArg指定为null,undefined,fun中的this指向window对象.
- 严格模式下:fun的this为undefined值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean
param1,param2(可选): 传给fun的参数。
- 如果param不传或为 null/undefined,则表示不需要传入任何参数.
- apply第二个参数为数组,数组内的值为传给fun的参数。
区别
- call是将第2~n的参数都传给fun,apply第2个参数是一个数组。
- call/apply调用一个具有给定this值的函数,是改变了函数this上下文后立刻执行该函数。而bind,bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this值执行为bind的第一个参数。并不是立即执行,还需要再调用该函数的执行。
例子
var a ={
name:'cherry',
age:'18',
say:function(school){
console.log(this.name+'_'+this.age+'_'+school);
}
}
var b={
name:'kitty',
age:'19'
}
a.say.call(b,'MIT1'); //call调用,绑定this值,并传入school参数
a.say.apply(b,['MIT2']); //apply调用,绑定this值,以数组形式传入school参数
a.say.bind(b,'MIT3')(); //bind调用,绑定this值,并传入school参数
2.手写实现三个函数
(1)手写实现call
思路
1.根据call的规则设置上下文对象(this指向)
2.通过设置context属性,将函数的this指向隐式绑定到context上
3.通过隐式绑定执行函数并传递参数
4.删除临时属性,返回函数执行结果
实现
Function.prototype.myCall = function (context, ...args) {
if(context === null || context === undefined){
context=window;
}//根据MDN中的定义,指定为null/undefin的this值会自动的指向全局对象(浏览器为window)
else{
context=Object(context); //值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
const fn = Symbol(); //返回Symbol类型的值,symbol值能作为对象属性的标识符,防止覆盖掉原有属性,用于临时存储函数
context[fn] = this; //函数的this隐式的绑定到context上
let result = context[fn](...args); //通过隐式绑定执行函数并传递参数
delete context[fn]; //删除上下文对象的属性
return result;
}
验证
//验证:
var a = {
name: 'cherry',
age: '18',
say: function (school){
console.log(this.name + '_' + this.age + '_' + school)
}
}
var b = {
name: 'kitty',
age: '19'
}
a.say.myCall(b,'MIT1')
结果
(2)手写实现apply
和call类似,只是参数传递方法不一样
Function.prototype.myCall = function (context, args) {
if(context === unll || context === undefined){
context=window;
}//根据MDN中的定义,指定为null/undefin的this值会自动的指向全局对象(浏览器为window)
else{
context=Object(context); //值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
}
const fn = Symbol(); //返回Symbol类型的值,symbol值能作为对象属性的标识符,防止覆盖掉原有属性,用于临时存储函数
context[fn] = this; //函数的this隐式的绑定到context上
let result = context[fn](...args); //通过隐式绑定执行函数并传递参数
delete context[fn]; //删除上下文对象的属性
return result;
}
(3)手写实现bind
思路
1.拷贝源函数:
(1)通过变量存储源函数
(2)使用Object.create复制源函数的prototype给newf
2.返回拷贝的函数
3.调用拷贝的函数:
(1)new调用判断:通过instanceof判断函数是否通过new调用,来决定绑定的context.
(2)绑定this+传递参数
(3)返回源函数的执行结果
实现
Function.prototype.myBind = function(context,...args1){
const self = this; //记录this值
let newf = function(){
//此时相当于 var result =new newf(),所以看this是不是newf的实例
//若为true:说明此时的this指向的是实例对象
//若不是,则是()直接调用的
const res=this instanceof newf? this : Object(context);
return self.apply(res,args1.concat(Array.prototype.slice.call(arguments)));
}
//利用原型链将他们串联起来,保证原函数的原型对象上的属性不能丢失。
newf.prototype = Object.create(self.prototype);
return newf;
}
测试结果
//要考虑bind具有两个不同的arguments对象
//在bind中this指向的是b
//var resultB=new B() //但是new后 this指向指向了resultB——>new操作后,this指向了实例化的对象
console.log("系统自带bind结果")
var A = a.say.bind(b, 'MITTT1')();
var B = a.say.bind(b,'MIT2')
var resultB = new B()
console.log("自己写的bind结果")
var C = a.say.myBind(b, 'MIT1')()
var D = a.say.myBind(b,'MIT3');
var resultD = new D();
二.new实现
1.new操作实现了什么
MDN:new运算符创建了一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
当执行new Foo(...)时会发生一下事情
- 创建一个继承自Foo.prototype的新对象。
- 使用指定的参数调用构造函数Foo,并将this绑定到新创建的对象上。
- 由构造函数返回的对象就是new表达式的结果。如果构造函数显式的返回了一个对象,则使用该对象作为返回结果,若没有显式的返回一个对象,则使用步骤1继承的新对象作为返回结果。
实现
function myNew(ctor, ...args) {
if (typeof ctor !== 'function') {
throw new TypeError('TypeError');
}
// 1.创建一个继承自ctor.prototype的新对象
const obj = Object.create(ctor.prototype);
// 2.使用指定参数调用构造函数,并绑定this
const res = ctor.apply(obj, args)
// 3.判断返回内容,若为返回对象则直接使用,若不是则使用新创建的对象
const isObj = typeof res === 'object' && res !== 'null';
const isFunc = typeof res === 'function';
return isObj || isFunc ? res : obj;
}
测试
//验证
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const car1 = new Car('Eagle', 'Talon TSi', 1993);
const car2 = myNew(Car, 'Kitty','TSi', 1997);
console.log(car1);
console.log(car2);
三.浅拷贝和深拷贝
1.浅拷贝
什么是浅拷贝?
创建一个新的对象,把原有的对象属性值,完整的拷贝过来。其中包括了原始类型的值还有引用类型的内存地址。
常见浅拷贝的方法
(1) Array.prototype.slice()
MDN定义:
slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
用法:
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
var result = animals.slice();
(2) Array.prototype.concat()
concat用于合并数组,但是也可以用来浅拷贝吧。
MDN: concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
(3) Object.assign()
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);// expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);// expected output: Object { a: 1, b: 4, c: 5 }
注意是浅拷贝,且拷贝的是属性值,并不拷贝对象本身。
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
console.log(obj2); //Object { a: 0, b: Object { c: 0 } }
obj2.b.c = 1;
console.log(obj1); //Object { a: 0, b: Object { c: 1 } }
console.log(obj2); //Object { a: 0, b: Object { c: 1 } }
(4) ES6中...args(扩展运算符) 扩展运算符(spread)是三个点(...)。将一个数组转为用逗号分隔的参数序列。
let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果
console.log(newArr) //Array [1, 2, 3]
手动实现浅拷贝
var shallowClone = (target) =>{
if (typeof target === 'object' && target !== null){
const cloneTarget = Array.isArray(target)? [] :{};
for (let prop in target){
if(target.hasOwnProperty(prop)){
cloneTarget[prop]=target[prop]
}
}
return cloneTarget;
}else{
return target;
}
}
2.深拷贝
一句话深拷贝实现
JSON.parse(JSON.stringify());
MDN中:
JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。
JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
手写实现
简易版本
var deepClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
} else {
return target;
}
}
解决循环引用问题
var deepClone=(target,map=new Map())=>{
if (map.get(target)){
return target;
}
if (typeof target === 'object' && target!==null){
map.set(target,true);
const cloneTarget=Array.isArray(target)?[]:{};
for (let prop in target){
if(target.hasOwnProperty(prop)){
//是否是其自身属性,而不是继承属性等
cloneTarget[prop]=deepClone(target[prop],map);
}
}
return cloneTarget;
}else{
return target;
}
}
三.节流防抖
- 防抖
在事件被触发n秒后再执行回调,如果n秒后又被触发,则重新计时
应用场景:
(1)search搜索框 (2)window触发resize时
//防抖
// 事件被触发n秒后再执行回调,如果在这n秒中又被触发,则重新计时。
// 应用场景:(1)点击请求上,避免用户因为用户多次点击向后后端发送多次请求(2)调整浏览器大小
const debounce = (fn,time) => {
let timeout=null;
return function(){
if(timeout) {
clearTimeout(timeout);
timeout = null
}
timeout = setTimeout (()=>{
fn.apply(this,arguments);
},time)
}
}
- 节流
在一个单位事件内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效。 应用场景:
(1)鼠标不断点击触发
(2)监听滚动事件
//节流
// 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行。如果在同一个单位时间内某事件被多次触发,只能有一次生效
// 应用场景:(1)scroll函数 (2)input框输入
const throttle = (fn,time) => {
let flag = true;
return function(){
if (!flag) return;
flag = false;
setTimeout(()=>{
fn.apply(this,arguments);
flag = true;
},time)
}
}
四.异步
1.Promise
参考es6.ruanyifeng.com/#docs/promi…
(1).什么是Promise
->Pomise中保存着某个未来才会结束的事件的结果。Promise是一个对象,从它可以获取异步操作的消息。
->Promise三种状态:Pending(进行中)、fulfilled(成功)、rejected(失败)。状态一旦发生改变,就不会再变。
(2)基本用法
->resolve函数,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject函数,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
->执行时间顺序:Promise立即执行,但是then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。
例子:
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
(3)Promise.prototype.then()
作用:为Promise实例添加状态改变时的回调函数。
返回:一个新的Promise实例。因此可以采用链式写法。若含有两个回调函数,则第一个回调函数完成后,会将返回结果作为参数,传入第二个回调函数中。
(4)Promise.prototype.finally()
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
(5) Promise.prototype.all()
方法:用于将多个Promise实例,包装成一个新的Promise实例。
(6)Promise.protitype.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
Promise解决方法
(借鉴:juejin.cn/post/684490…
Promise利用三大技术手段来解决回调地狱。
(1)回调函数延迟绑定(回调函数是通过后面的then方法传入的)
(2)返回值穿透(根据 then 中回调函数的传入值创建不同类型的Promise, 然后把返回的 Promise 穿透到外层, 以供后续的调用。)
(3)错误冒泡(错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了)
Promise实现
- 代码实现:
五.事件流
1.事件捕获和冒泡
(1)HTML与JS交互式通过事件驱动来实现的。
(2)事件流的三个阶段:事件捕获、目标阶段、事件冒泡
事件捕获:从外到里。
事件冒泡:从里到外。开始时由最具体的元素接收,然后逐级向上传播到DOM最顶层节点。
addEventListner
注意:某些事件没有冒泡:onblur,onfocus,onmouseenter,onmouseleave
2.事件对象
event
div.onclick = function(event){}
div.addEventListener('click',function(event){})
//1.event 就是一个事件对象,写到侦听函数中(写到小括号中,当形参来看。)
//2.事件对象只有有了事件才会存在,是系统给我们自动创建的,不需要传递参数。
//3.事件对象:是事件一系列相关数据的集合。跟事件相关。(比如鼠标点击:包含鼠标相关信息,比如坐标什么的)
//4.兼容性:event = event || document.event
3.事件对象常见的属性和方法
(1)event.target:返回触发事件的对象。(兼容性:event.srcElement)
target和this的区别:
target指向的是触发事件的对象(元素),this指向的是绑定事件的对象(元素)。
<!-- 布局 -->
<ul>
<li>li1</li>
<li>li2</li>
<li>li3</li>
</ul>
<!-- 事件 -->
<script>
var ul = document.querySelector('ul');
ul.addEventListener('click',function(event){
console.log(this);
console.log(event.target);
})
</script>
执行结果:
4.阻止事件
阻止默认行为:
event.preventDefault() 阻止默认行为事件,比如链接不跳转,按钮不提交等等。
阻止冒泡:
event.stopPropagation() :
兼容性(IE678:window.event.cancelBubble())
例子:正常情况下:
<div class="father">
<div class="son">sonson</div>
</div>
<!-- 事件 -->
<script>
var son = document.querySelector('.son');
son.addEventListener('click',function(){
console.log('son');
},false);
var father = document.querySelector('.father');
father.addEventListener('click',function(){
console.log('father');
},false);
document.addEventListener('click',function(){
console.log('document');
},false);
</script>
常规结果:
增加阻值冒泡事件后:在son中增加stopPropagation();
var son = document.querySelector('.son');
son.addEventListener('click',function(){
console.log('son');
stopPropagation();
},false);
结果:
5.事件委托
原理:
不给每个子节点单独的设置事件监听器,而是将事件监听器设置在其父节点上,然后利用冒泡原理影响每个子节点。(例:上面的例子,给ul注册点击事件,然后利用事件对象的target来找到当前点击的li,因为点击li,事件会冒泡到ul上,ul有注册事件,就会触发事件监听器。)
作用:
只操作一次DOM,提高程序的性能。
例子:
<!-- 布局 -->
<ul>
<li>li1</li>
<li>li2</li>
<li>li3</li>
<li>li4</li>
<li>li5</li>
</ul>
<!-- 事件 -->
<script>
// 事件委托的核心:给父节点添加侦听器,利用事件冒泡影响每一个子节点
var ul = document.querySelector('ul');
ul.addEventListener('click',function(event){
// e.target获取我们的点击对象
console.log(event.target.innerHTML);
})
</script>
六.变量/函数提升
1.变量声明提升:
通过var定义(声明)变量,在定义语句之前就可以访问到。
值:undefined
2.函数声明提升
通过function声明的函数,在之前就可以直接调用。
值:函数定义(对象)
函数提升在变量提升之上
3.原因:
因为JS首先会进行预处理:会创建一个词法环境,扫描JS中用生命方式声明的函数和变量,并将它们加到预处理阶段的词法环境中去。
七.Event Loop 事件循环
(图片引用以及后面文章一些借鉴的内容:segmentfault.com/a/119000001…
事件循环中的一些概念梳理:
1.MacroTask宏任务
script全部代码、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。
2.微任务
Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver。
执行顺序
1.一开始整段脚本作为第一个宏任务执行
2.执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列。
3.当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空。
(4.执行浏览器 UI 线程的渲染工作
5.检查是否有Web worker任务,有则执行 )
(4和5还不是很理解)
6.执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
测试
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9);
})
console.log(10);
//result:1,4,10,5,6,7,2,3,9,8
八.ES6新特性
1.let
let实际上为JS新增了块级作用域。
(摘自es6.ruanyifeng.com)
(1)let 声明变量,只在let命令所在的代码块内有效。
常见let和var的区分:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
原因:上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
使用let后:
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
(2)let不存在变量提升
(3)let 存在暂时性死区
即在代码块内,使用let命令声明变量之前,该变量都是不可用的。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError 即使前面有全局变量也不ok
let tmp;
}
2.const
const声明一个只读的常量,一旦声明不可以更改,必须立即初始化。
const的作用域只在声明所在的块级作用域内有效(同let)
3.Symbol
引入本质:防止对象的属性名冲突。
Symbol可以显式的转换为字符串sym.toString() // 'Symbol(My symbol)',也可以转换为布尔值(但是不能转换为数字)
因为Symbol()值不相等,意味着Symbol值可以用来作为标识符,用于对象的属性,能防止某一个键不小心被改写或者覆盖。(例如本文章上面的重新call中的用法)
let mySymbol = Symbol();
let a = {};
a[mySymbol] = 'Hello!';
4.Set()和Map()
1.Set()
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set()可以通过add()方法向Set结构中加入成员,Set函数也可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化。Set加入值时也不会发生类型转换(类似于===,5和"5"被认为是不相等。不同的是多次添加NaN只能添加一次。)
const set = new Set([1, 2, 3, 4, 4]);
[...set] // [1, 2, 3, 4]
2.Map()
键值对的集合。
也可以接受数组(或者任何具有Iterator接口,且每个成员都是一个双元素的数据结构)作为参数
3.前端常考面试题,Set和Map有什么区别?
Map是一组键值对的结构,Set是一组key的集合(不存储value),特点是在Set中没有重复的key。
5.for...of...
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
6.箭头函数
(1)箭头函数没有this值,这个函数中的this取决外面函数的this值。
(2)不能用于构造函数,没有prototype值,没有arguments。