一、实现 call(Function.prototype.call)
call核心:
- 第一个参数是null或者undefined时,默认上下文为全局对象window
- 接下来给context创建一个fn属性,并将值设置为需要调用的函数
- 为了避免函数和上下文(context)的属性发上冲突,使用Symbol类型
- 调用函数
- 函数执行完成后删除context.fn属性
- 返回执行结果
Function是构造函数、Function.prototype 构造函数的原型对象,这个原型对象中包含很多方法,所以模仿call就在这个原型对象上添加方法就好了
Function.prototype.call = function(context = window, ...args){
if (typeof this !== 'function') {
throw new TypeError('Type Error')
}
const fn = Symbol('fn');
context[fn] = this;
const res = context[fn](args);
delete context[fn];
return res;
}
二、实现 apply
apply核心:
- 前面的步骤和call基本一致
- 第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function(context = windowm, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Type Error');
}
const fn = Symbol('fn');
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
三、实现bind:
-
对于普通函数,绑定this指向
-
对于构造函数,要保证原函数的原型对象上的属性不能丢失
会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
Function.prototype.bind = function(context, ...args) {
if (typeof this !== 'function') {
throw new Error("Type Error");
}
// 保存this的值
var self = this;
return function F() {
// 考虑new的情况
// 对于普通函数,绑定this指向
// 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象
if(this instanceof F) {
return new self(...args, ...arguments)
}
return self.apply(context, [...args, ...arguments])
}
}
总结:call apply bind的作用和区别
call、apply、bind的作用都是改变函数运行时的this指向。 bind和call、apply在使用上有所不同,bind在改变this指向的时候,返回一个改变执行上下文的函数,不会立即执行函数,而是需要调用该函数的时候再调用即可,但是call和apply在改变this指向的同时执行了该函数。 bind只接收一个参数,就是this指向的执行上文。 call、apply接收多个参数,第一个参数都是this指向的执行上文,后面的参数都是作为改变this指向的函数的参数。但是call和apply参数的格式不同,call是一个参数对应一个原函数的参数,但是apply第二个参数是数组,数组中每个元素代表函数接收的参数,数组有几个元素函数就接收几个元素。
call的应用场景:
- 对象的继承,在子构造函数这种调用父构造函数,但是改变this指向,就可以继承父的属性 function
superClass () {
this.a = 1;
this.print = function () { console.log(this.a); } }
function subClass () {
superClass.call(this); // 执行superClass,并将superClass方法中的this指向subClass
this.print();
}
subClass();
- 借用Array原型链上的slice方法,把伪数组转换成真数组
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("div"));
apply的应用场景:
- Math.max,获取数组中最大、最小的一项
let max = Math.max.apply(null, array);
let min = Math.min.apply(null, array);
- 实现两个数组合并
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6]
bind的应用场景:
在vue或者react框架中,使用bind将定义的方法中的this指向当前类
四、实现一个 new 操作符
首先先来了解一下new 一个对象的过程
function Person(name) {
this.name = name
}
let son = new Person('小明');
1.创建一个对象
2.使空对象__proto__指向构造函数的原型(prototype)
3.新对象和函数调用的this会绑定起来,也就是把this绑定到空对象;
4.执行构造函数中的代码,为空对象添加属性
5.判断函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
了解这个流程以后如何手写一个new呢?
function newOperator(ctor, ...args) {
if (typeof ctor !== 'functoin') {
throw new TypeError('Type ERROR')
}
let obj = {}; // 1
ctor.__proto__ = obj.prototype; // 2
let res = ctor.apply(obj, args); // 3、4
let isObject = res instanceof Object && res !== null;
let isFunction = res instanceof Function;
return isObject || isFunction ? res : obj
}
五、instanceof
实现原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false。
const myInstanceof = (left, right) => {
// 由于instanceof的检测方法是判断检测的类型是否在当前实例的原型链上,不太适用于检测基本数据类型
if(left !== 'object' || left === null) return false;
let leftValue = left.__proto__
let rightValue = right.prototype
while(true) {
if(leftValue === null) return false
if(leftValue === rightValue) return true
leftValue = leftValue.__proto__
}
}
leftValue = leftValue.__proto__
扩展: JavaScript判断变量的类型的几种方法
一共有4种方法判断变量的类型,分别是typeof、instanceof、Object.prototype.toString.call()(对象原型链判断方法)、 constructor (用于引用数据类型)
typeof:常用于判断基本数据类型,对于引用数据类型除了function返回’function‘,其余全部返回’object'。 instanceof:主要用于区分引用数据类型,检测方法是检测的类型在当前实例的原型链上,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来。
constructor:用于检测引用数据类型,检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。 Object.prototype.toString.call():适用于所有类型的判断检测,检测方法是Object.prototype.toString.call(数据) 返回的是该数据类型的字符串。 Object.prototype.toString.call()原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果
这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。
六、 深拷贝
- 判断类型是否为原始类型,如果是,无需拷贝,直接返回
- 为避免出现循环引用,拷贝对象时先判断存储空间中是否存在当前对象,如果有就直接返回
- 开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
- 对引用类型递归拷贝直到属性为原始类型
const deepClone = (target, cache = new weakMap()) => {
if(target === null || typeof target !== object) {
return target;
}
if (cache.get(target)) {
return target;
}
const copy = Array.isArray(target) ? [] : {};
cache.set(target, copy);
Object.keys(target).forEach(key => copy[key] = deepClone(target[key],cache))
return copy
}
扩展: WeakMap
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能作为 key 的(比如 Symbol)。
WeakMap具有的方法:
- WeakMap.prototype.delete(key):移除 key 的关联对象。执行后 WeakMap.prototype.has(key) 返回false。
- WeakMap.prototype.get(key):返回 key 关联对象,或者 undefined(没有 key 关联对象时)。
- WeakMap.prototype.has(key):根据是否有 key 关联对象返回一个布尔值。
- WeakMap.prototype.set(key, value):在 WeakMap 中设置一组 key 关联对象,返回这个 WeakMap 对象。
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function(){},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
扩展: 常用深拷贝方法
- JSON.parse(JSON.stringify())```
七、数组扁平化
数组扁平化是指将一个多维数组变为一个一维数组
方法一 : 使用flat()
const res1 = arr.flat(Infinity);
方法二: 利用正则, 但数据类型都会变为字符串
const res2 = JSON.stringify(arr).replace(/[|]/g, '').split(',');
方法三: 正则改良版本
const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/[|]/g, '') + ']');
方法四: 使用reduce
const flatten = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
const res4 = flatten(arr);
方法五:函数递归
const res5 = [];
const fn = arr => {
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
fn(arr[i]);
} else {
res5.push(arr[i]);
}
}
}
fn(arr);
方法六:技巧变换
const str = [0, 1, [2, [3, 4]]].toString()
// '0, 1, 2, 3, 4'
const arr = str.split(',')
// ['0','1','2', '3', '4']
const newArr = arr.map(item => +item)
// [0, 1, 2, 3, 4]
const flatten = (arr) => arr.toString().split(',').map(item => +item)
八、数组去重
方法一: 利用Set
****const res1 = Array.from(new Set(arr));
方法二: 两层for循环+splice
const unique1 = arr => {
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
// 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
len--;
j--;
}
}
}
return arr;
}
方法三: 利用indexOf
const unique2 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
方法四: 利用include
const unique3 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
方法五: 利用filter
const unique4 = arr => {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
方法六: 利用Map
const unique5 = arr => {
const map = new Map();
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) {
map.set(arr[i], true)
res.push(arr[i]);
}
}
return res;
}
九、继承
寄生组合继承
function Super(foo) {
this.foo = foo
}
Super.prototype.printFoo = function() {
console.log(this.foo)
}
function Sub(bar) {
this.bar = bar
Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
ES6版继承
class Super {
constructor(foo) {
this.foo = foo
}
printFoo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
super(foo)
this.bar = bar
}
}
十、类数组转换为数组
类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。
方法一:Array.from
Array.from(document.querySelectorAll('div'))复制代码
方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))复制代码
方法三:扩展运算符
[...document.querySelectorAll('div')]复制代码
方法四:利用concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
十一、AJAX
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
xhr.setRequestHeader('Accept', 'application/json');
十二、函数柯里化
指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用。
// 原理是利用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
const currying = fn =>
_curry = (...args) =>
args.length >= fn.length
? fn(...args)
: (...newArgs) => _curry(...args, ...newArgs)
参考文章: 作者:洛霞同学 链接:juejin.cn/post/687515…