手写常用方法
数据类型判断
🔥 typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。
typeof undefined // 'undefined'
typeof Symbol.for("a") // 'symbol'
typeof (()=> 1) // 'function'
typeof null // 'object'
typeof {} // 'object'
Object.prototype.toString.call(1) // '[object Number]'
2
3
4
5
6
7
🚀 实现一个类型判断方法:
function typeOf(param) {
return Object.prototype.toString.call(param).slice(8, -1);
}
typeOf("") // 'String'
typeOf(Symbol()) // 'Symbol'
typeOf(()=> 1) // 'Function'
typeOf({}) // 'Object'
typeOf(null) // 'Null'
typeOf(undefined) // 'Undefined'
typeOf(new Date()) // 'Date'
2
3
4
5
6
7
8
9
10
11
继承
原型链继承
通过构造函数的prototype指向原型对象
function Person() {
this.colors = ['yellow', 'white']
}
Person.prototype.getColors = function() {
return this.color;
}
function Doctor() {};
Doctor.prototype = new Person();
// ----------Testing----------
const doc = new Doctor();
doc.colors.push('blue');
const doc1 = new Doctor();
console.log(doc1.colors); // ['yellow', 'white', 'black']
doc instanceof(Doctor); // true
doc instanceof(Person); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
原型链继承:
- 🚨 原型中包含的引用类型属性将被所有实例共享;
- 🚨 子类在实例化的时候不能给父类构造函数传参;
借用构造函数实现继承
function Person(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
function Lawyer(name) {
// 为Lawyer实例对象添加Person的属性
Person.call(this, name);
}
Lawyer.prototype = new Person();
// ----------Testing----------
const ll = new Lawyer("Michael")
2
3
4
5
6
7
8
9
10
11
12
13
14
借用构造函数实现继承:
🎉 由于每次实例化子类都重新创建了一次属性,所以不再存在引用类型共享的问题
🎉 通过
call
方法调用父类构造器,允许在实例化的时候往父类传参🚨 子类实例每次都要创建一次方法,内存开销大
组合继承
🚀 组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.getName = function() {
return this.name;
}
function Dog(name, age) {
// 子类实例上声明父类实例的属性
Animal.call(this, name);
this.age = age;
}
// 原型继承,获得父类原型对象方法的访问能力
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
// ----------Testing----------
let dog1 = new Dog('奶昔', 2);
dog1.colors.push('brown');
// { name: "哈赤", colors: ["black", "white", "brown"], age: 1 }
let dog2 = new Dog('哈赤', 1);
// { name: "哈赤", colors: ["black", "white"], age: 1 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
寄生式组合继承 ✅
🔥 组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。
🚀 所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本,区别如下:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
2
3
4
5
6
7
8
稍微封装下上面添加的代码后:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Dog, Animal)
2
3
4
5
6
7
8
9
10
11
🎉 其实这就是Object.create
方法的实现,可以基于组合继承的代码改成最简单的寄生式组合继承:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
2
3
4
5
最终代码
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.getName = function() {
return this.name;
}
function Dog(name, age) {
// 子类实例上声明父类实例的属性
Animal.call(this, name);
this.age = age;
}
// 原型继承,获得父类原型对象方法的访问能力
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let dog1 = new Dog('奶昔', 2);
dog1.colors.push('brown');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class 实现继承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
函数柯里化
柯里化
- 柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。
- 柯里化是一种函数的转换,它是指将一个函数从可调用的
f(a, b, c)
转换为可调用的f(a)(b)(c)
。 - 柯里化不会调用函数。它只是对函数进行转换。
// 高级柯里化实现
function curry(fn) {
return function curried(...args) {
// 闭包内保存着 args
if (args.length >= fn.length) {
return fn(...args);
}
return function (...subArgs) {
// args 需要更新
return curried(...args.concat(subArgs));
};
};
}
function plus(a, b, c) {
return a + b + c;
}
const hello = curry(plus);
console.log(hello(1, 2)(5)); // 8
console.log(hello(1)(2, 5)); // 8
console.log(hello(1)(2)(5)); // 8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
✅ 只允许确定参数长度的函数
柯里化要求函数具有固定数量的参数。
使用 rest 参数的函数,例如
f(...args)
,不能以这种方式进行柯里化。✅ 比柯里化多一点
根据定义,柯里化应该将
sum(a, b, c)
转换为sum(a)(b)(c)
。但是,如前所述,JavaScript 中大多数的柯里化实现都是高级版的:它们使得函数可以被多参数变体调用。
数组去重
双层for循环
function distinct(arr) {
for (let i = 0, len = arr.length; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] == arr[j]) {
arr.splice(j, 1)
// splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
len--
j--
}
}
}
return arr
}
2
3
4
5
6
7
8
9
10
11
12
13
分析:双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2)
,如果数组长度很大,效率会很低
Array.filter() 加 indexOf
ES5 利用indexOf方法返回寻第一个匹配下标的特性
const unique2 = (arr) => arr.filter((item, index, array)=> array.indexOf(item) === index);
ES6 中的 Set 去重
const unique1 = (arr) => [...new Set(arr)];
Object 键值对
时间复杂度最小,但是空间复杂度较大
function distinct(array) {
var obj = {}
return array.filter(function(item, index, array) {
return obj.hasOwnProperty(typeof item + item)
? false
: (obj[typeof item + item] = true)
})
}
2
3
4
5
6
7
8
这种方法是利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值,比如 Object[value1] = true
,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的,但是最后请注意这里obj[typeof item + item] = true
没有直接使用obj[item]
,是因为 因为 123 和 '123' 是不同的,直接使用前面的方法会判断为同一个值,因为对象的键值只能是字符串
,所以我们可以使用 typeof item + item
拼成字符串作为 key 值来避免这个问题。
数组扁平化
🔖 数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。Array.prototype.flat
可以直接将多层数组拍平成一层:
[1, [2, [3]]].flat(2); // [1, 2, 3] 参数2表示铺平的层级
ES5 实现:递归即可
function flat(arr) {
let result = []
for(let i = 0; i<arr.length; i++) {
const item = arr[i]
if(Array.isArray(item)) {
result = result.concat(flat(item))
}else {
result.push(item)
}
}
return result
}
// ----------Testing----------
flat([1,2,[3,4, [5]]]) // [1, 2, 3, 4, 5]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ES6 实现
// [].concat(1,2,3,[4,5]) [1, 2, 3, 4, 5]
// concat的参数是以逗号分分隔的任何数据类型,一维数组会直接被解构推入到新数组当中
function flat(arr) {
while(arr.some(i=> Array.isArray(i))) {
arr = [].concat(...arr)
}
return arr
}
}
// ----------Testing----------
flat([1,2,[3,4, [5]]]) // [1, 2, 3, 4, 5]
2
3
4
5
6
7
8
9
10
11
12
深浅拷贝
浅拷贝
只处理第一层数据,对对象类型数据进行拷贝
function shallowCopy(obj) {
if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
2
3
4
5
6
7
8
9
10
11
深拷贝
基础版:只考虑普通对象属性,不考虑内置对象和函数。(递归浅拷贝)
function deepClone(obj) {
if (typeof obj !== 'object') return;
const newObj = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
2
3
4
5
6
7
8
9
10
复杂版深拷贝:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 获取当前值的构造函数:获取它的类型
let constructor = target.constructor;
// 检测当前对象target是否与正则、日期格式对象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 创建一个新的特殊对象(正则类/日期类)的实例
return new constructor(target);
}
if (isObject(target)) {
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
事件总线(发布订阅)
观察者模式
一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件。(观察者自身维护着事件)
大白话解释:当去你去医院打吊瓶(某些地方叫打点滴,就是这么严谨)的时候,医生要观察吊瓶的改变,当快打完的时候,就要通知医生来取针。这里医生就是观察者(Obeserver),吊瓶就是被观察者/主题。
class Subject { // 被观察者
constructor(name){
this.state = 100;
this.name = name;
this.obs = [] // 会有多个观察者
}
addObs(ob){
this.obs.push(ob)
}
setState(state){ // 改变状态的方法
this.state = state
// 通知观察者执行动作
this.obs.forEach((ob)=>{
ob.update(this) // 让观察者自身去做更新
})
}
}
class Obeserver{ // 观察者
constructor(name){
this.name = name;
}
update(subject){
// 这里观察者也有可能观察多个
if (!subject.state) {
console.log(`${this.name} 收到通知 :${subject.name} 的 带瓶打完啦!`)
}else {
console.log(` ${this.name} 收到通知 :${subject.name} 的 带瓶量: ${subject.state}!`)
}
}
}
var zhiliao = new Subject();
var hushi = new Obeserver("护士");
var yisheng = new Obeserver("医生")
zhiliao.addObs(hushi) // 把观察者放到被观察这的里面去
zhiliao.addObs(yisheng)
zhiliao.setState(50)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
发布订阅
订阅者把自己想要订阅的事件注册到调度中心,当发布者发布事件到调度中心(就是该事件被触发),再由调度中心统一调度订阅者注册到调度中心的处理代码。
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
解析 URL 参数为对象
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
2
3
4
5
6
7
8
9
测试:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12
}
render(template, person); // 我是布兰,年龄12,性别undefined
2
3
4
5
6
图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
- 图片全部加载完成后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
// 修正错误,需要加上自执行
const imgLazyLoad = (function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
})()
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
参考:图片懒加载在新窗口打开
函数防抖
防抖,debounce,后执行
触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。
简单版
函数内部支持使用 this 和传递参数;
function debounce(fn, timeout = 1000) {
let timer = null;
return function (...args) {
const context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(fn.bind(context, ...args), timeout);
};
}
2
3
4
5
6
7
8
9
10
使用:
function fn1(...args) {
console.log(this);
console.log(args);
}
const obj = {
name: "hello",
action: debounce(fn1, 3000),
};
obj.action(1);
obj.action(2);
obj.action(3);
obj.action(4);
// { name: 'hello', action: [Function (anonymous)] }
// [ 4 ]
// [Done] exited with code=0 in 3.056 seconds
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最终版
除了支持 this 和参数外,还支持以下功能:
- 支持立即执行;
- 函数可能有返回值;(只有立即执行才能拿到返回值)
- 支持取消;
function debounce(fn, timeout, immediate = false) {
let timer, result;
const debouncedFn = function (...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate) {
immediate = false;
result = fn.apply(context, args);
}
timer = setTimeout(fn.bind(context, ...args), timeout);
return result;
};
debouncedFn.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debouncedFn;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
使用:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1" />
<title>debounce</title>
<style>
#container {
width: 100%;
height: 200px;
line-height: 200px;
text-align: center;
color: #fff;
background-color: #444;
font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<button id="button">点击取消debounce</button>
<script>
function debounce(fn, timeout, immediate = false) {
let timer, result;
const debouncedFn = function (...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate) {
immediate = false;
result = fn.apply(context, args);
}
timer = setTimeout(fn.bind(context, ...args), timeout);
return result;
};
debouncedFn.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debouncedFn;
}
let count = 1;
let container = document.getElementById("container");
function addCount() {
container.innerHTML = count++;
return count;
}
let debounceFn = debounce(addCount, 1000, true);
container.onmousemove = function () {
var res = debounceFn();
console.log(res);
};
document.getElementById("button").addEventListener("click", function () {
debounceFn.cancel();
});
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
函数节流
触发高频事件,且 N 秒内只执行一次。
简单版:使用时间戳来实现,立即执行一次,然后每 N 秒执行一次。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
最终版:支持取消节流;另外通过传入第三个参数,options.leading 来表示是否可以立即执行一次,opitons.trailing 表示结束调用的时候是否还要执行一次,默认都是 true。 注意设置的时候不能同时将 leading 或 trailing 设置为 false。
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var 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;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
手写 new 方法
new 操作符
- 首先创建了一个新对象 设置原型
- 将对象的原型设置为函数的prototype对象(继承父类原型上的方法)
- 让函数的this指向这个对象,执行构造函数的代码(添加父类的属性到新的对象上并初始化)
- 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是对象(函数也是对象),就返回这个对象
function myNew(constructor, ...args) {
if (typeof constructor !== "function") {
throw "myNew方法的第一个参数必须是一个方法";
}
// 基于constructor的原型创建一个全新的对象
let newObj = Object.create(constructor.prototype);
// 获取传入的参数
// let args = Array.from(arguments).slice(1);
// 添加属性到新创建的newObj上, 并获取obj函数执行的结果
let result = constructor.apply(newObj, args);
// 判断result类型,如果是object或者function类型,则直接返回结果
let resultType = Object.prototype.toString.call(result);
if (resultType === '[object Object]' || resultType === '[object Function]') {
return result;
}
return newObj;
}
function Person(firtName, lastName) {
this.firtName = firtName;
this.lastName = lastName;
}
Person.prototype.getFullName = function () {
return `${this.firtName} ${this.lastName}`;
};
const a = myNew(Person, 'Chen', 'Jinrui')
console.log(a.getFullName()) // Chen Jinrui
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
手写 instanceof
function Person(name) {
this.name = name;
}
const jc = new Person("Hello");
console.log(jc instanceof Person); // true
console.log(jc instanceof Object); // true
console.log(jc instanceof Array); // false
function myInstanceOf(obj, origin) {
if (typeof obj !== 'object' || obj === null) return false;
while (obj.__proto__ !== null) {
if (obj.__proto__ === origin.prototype) {
return true;
}
obj = obj.__proto__;
}
return false;
}
console.log(myInstanceOf(jc, Person)); // true
console.log(myInstanceOf(jc, Object)); // true
console.log(myInstanceOf(jc, Array)); // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
手写 call 方法
不得使用 apply、bind 函数辅助
Function.prototype.myCall = function (ctx, ...args) {
ctx = ctx == null ? globalThis : Object(ctx);
var key = Symbol("fn");
Object.defineProperty(ctx, key, {
enumerable: false,
value: this, // method.myCall 时 this 即为 method 函数
});
var result = ctx[key](...args);
delete ctx.key;
return result;
};
function method(a, b) {
console.log(this, a, b);
return a + b;
}
console.log(method.myCall(null, 1, 2)); // Object [global] 1 2; 3;
console.log(method.myCall({}, 2, 2)); // {} 2 2; 4
console.log(method.myCall(1, 2, 2)); // [Number: 1] 2 2; 4
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
手写 bind 方法
Function.prototype.myBind = function (ctx) {
var fn = this;
return function (...args) {
return fn.apply(ctx, args);
};
};
function fn(a, b) {
console.log(this, a, b);
return a + b;
}
const newFn = fn.myBind({});
console.log(newFn(2, 2)); // {} 2 2; 4;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function (ctx, ...args) {
var fn = this;
return function (...args1) {
return fn.call(ctx, ...args, ...args1);
};
};
function fn(a, b) {
console.log(this, a, b);
return a + b;
}
const newFn = fn.myBind({}, 1);
console.log(newFn(2)); // {} 1 2; 3;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var slice = Array.prototype.splice
Function.prototype.myBind = function (ctx) {
var fn = this;
// 因为arguments 不是数组,没有slice方法,所以用slice.call(arguments)
var args = slice.call(arguments, 1); // 获取除了第0个参数this,之外的所有其他参数
if (typeof fn !== "function") {
throw new Error("bind必须调用在函数上");
}
return function () {
var args2 = slice.call(arguments, 0); // 获取所有参数
// 用apply传数组,合并两次的参数数组,用apply传
return fn.apply(ctx, args.concat(args2));
};
};
function fn(a, b) {
console.log(this, a, b);
return a + b;
}
const newFn = fn.myBind({}, 1);
console.log(newFn(2)); // {} 1 2; 3;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Function.prototype.myBind = function (ctx, ...args) {
// this 就是函数
var fn = this;
function resultFn(...args2) {
// 也可以用这句话判断是否用了 new
// resultFn.prototype.isPrototypeOf(this);
// 判断是否用了 new, 对 this 做不同处理
return fn.call(this instanceof resultFn ? this : ctx, ...args, ...args2);
}
// 重新绑定原型
resultFn.prototype = fn.prototype;
return resultFn;
};
function fn(a, b) {
this.a = a;
this.b = b;
}
const newFn = fn.myBind({}, 1);
console.log(new newFn(2)); // fn { a: 1, b: 2 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var slice = Array.prototype.slice;
Function.prototype.myBind = function (ctx) {
var args = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") {
throw new Error("bind 必须调用在函数身上");
}
function resultFn() {
var args2 = slice.call(arguments, 0);
return fn.apply(
resultFn.prototype.isPrototypeOf(this) ? this : ctx,
args.concat(args2)
);
}
resultFn.prototype = fn.prototype;
return resultFn;
};
function fn(a, b) {
this.a = a;
this.b = b;
}
const newFn = fn.myBind({}, 3);
console.log(new newFn(4)); // fn { a: 3, b: 4 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
手写元素拖拽
const list = document.querySelector(".list");
let sourceNode;
list.ondragstart = (e) => {
// 拖拽元素样式就是拖拽发生时的样式,立即修改会导致拖拽的也跟着发生变化
setTimeout(() => {
e.target.classList.add("moving");
}, 0);
sourceNode = e.target;
// 拖拽的默认行为是复制,可以改成移动
e.dataTransfer.effectAllowed = "move";
};
list.ondragenter = (e) => {
e.preventDefault();
if (e.target === list || e.target === sourceNode) {
return;
}
const children = Array.from(list.children);
const sourceIndex = children.indexOf(sourceNode);
const targetIndex = children.indexOf(e.target);
if (sourceIndex < targetIndex) {
list.insertBefore(sourceNode, e.target.nextElementSibling);
} else {
list.insertBefore(sourceNode, e.target);
}
};
// 默认行为下很多元素不允许元素拖动到自己身上,导致鼠标放开的时候元素动画回到了原始位置
// 通过阻止 ondragenter、ondragover 的默认行为解决
list.ondragover = (e) => {
e.preventDefault();
};
list.ondragend = (e) => {
e.target.classList.remove("moving");
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.list {
list-style: none;
margin: 0 auto;
padding: 0;
width: 50vw;
}
.list-item {
background-color: #34495e;
margin-bottom: 10px;
cursor: move;
user-select: none;
height: 45px;
line-height: 45px;
color: white;
text-align: center;
border-radius: 4px;
box-sizing: border-box;
}
.list-item.moving {
background: transparent;
color: transparent;
border: 1px dashed #ccc;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<ul class="list">
<li draggable="true" class="list-item">1</li>
<li draggable="true" class="list-item">2</li>
<li draggable="true" class="list-item">3</li>
<li draggable="true" class="list-item">4</li>
<li draggable="true" class="list-item">5</li>
</ul>
2
3
4
5
6
7