JS 手写代码题汇总

  • 记录一些最近看到/以前遇到的手写代码题
  • 参考文章

数组

数组去0(2种方法)

  • 要求:将数组中的0去掉,给0计数,返回新数组。
  • 方法1:es6 array.filter()数组过滤方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function handleArr(arr) {
    let newArr = arr.filter((key) => key !== 0);
    let sum = 0;
    for (key of arr) {
    if (key === 0) {
    sum++;
    }
    }
    return { newArr, sum };
    }
    console.log(handleArr(arr).newArr, handleArr(arr).sum); // [1, 13, 45, 5, 16, 6, 25, 4, 17, 6, 7, 15] 4
  • 方法2:for循环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 题目
    let arr = [1, 13, 45, 5, 0, 0, 16, 6, 0, 25, 4, 17, 6, 7, 0, 15];
    // 答案
    function handleArr(arr) {
    let newArr = [],
    sum = 0;
    for (key of arr) {
    if (key === 0) {
    sum++;
    } else {
    newArr.push(key);
    }
    }
    return {newArr, sum};
    }
    console.log(handleArr(arr).newArr,handleArr(arr).sum); // [1, 13, 45, 5, 16, 6, 25, 4, 17, 6, 7, 15] 4

数组去重

可参考“前端面试题目(2)”

ES6 Set

  • 注意:Set对象不是数组,需要转换为数组(有2种方法转换)
    • 方法1:扩展运算符
    • 方法2:Array.from()把set结构转换为数组
  • 代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33];
    function unique(arr) {
    // ES6 Set
    let newSet = new Set(arr);
    // Set对象不是数组,需要转换为数组。
    // 方法1:扩展运算符;方法2:Array.from()把set结构转换为数组
    // return [...newSet];
    return Array.from(newSet);
    }
    console.log(unique(arr));

不创建新数组

1
2
3
4
5
6
7
8
// 方法1:注意Set对象不是数组,需要转换为数组
arr=[...new Set(arr)];

// 方法2:
function unique(array) {
//Array.from()能把set结构转换为数组
return Array.from(new Set(array));
}
  • 原地操作数组,删除重复元素,可参考“ES6 Map和Set”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33];
    function unique(arr) {
    // 原地操作数组,删除重复元素
    let len = 0; // len为不重复数的index,结果数组长度len
    // 遍历原数组
    for (let i = len; i < arr.length; i++) {
    // 原数组中第一次找到的数组项放入数组前方,len长度+1
    if (arr.indexOf(arr[i]) === i) {
    arr[len] = arr[i];
    len++;
    }
    // 相同的i+1继续查
    }
    // 去重后,删除数组后方多余数据项
    arr.splice(len);
    // 原地修改,不需要返回
    }
    // 针对 原地修改,不返回数组 的打印
    unique(arr);
    console.log(arr);//  [1, 2, 3, 4, 5, 11, 22, 33]

JS 三种方法

  • 方法1:最简单数组去重法(遍历结果数组,没有的就加进结果数组中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33];
    function unique(arr) {
    // 遍历结果数组
    let newArr = [];
    for (key of arr) {
    // 新数组中无则放入新数组
    if (newArr.indexOf(key) < 0) {
    newArr.push(key);
    }
    }
    return newArr;
    }
    console.log(unique(arr));
  • 方法2:速度最快, 占空间最多(空间换时间),参考“前端面试题目(2)”

  • 方法3:数组下标判断法(遍历原数组,符合的就加进结果数组中)

    • 注意:indexOf遇到多个时返回第一个的位置
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33];
      function unique(arr) {
      // 遍历原数组
      let newArr = [];
      for (let i = 0; i < arr.length; i++) {
      // 在原数组中是indexOf时第一个的则放入结果数组
      if (arr.indexOf(arr[i]) === i) {
      newArr.push(arr[i]);
      }
      }
      return newArr;
      }
      console.log(unique(arr));

数组排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 题目:将data中的数据按照age 从小到大排序
var data = [
{ name: 'Jay', age: 10 },
{ name: 'Jerry', age: 15 },
{ name: 'Merry', age: 12 },
{ name: 'Tom', age: 14 },
{ name: 'Richard', age: 22 }
];
// 答案【sort,小升大降】
function compare(prop) {
return function (obj1, obj2) {
var value1 = obj1[prop];
var value2 = obj2[prop];
return value1 - value2;
}
}
data.sort(compare('age'));
// 极简写法
data.sort((a,b)=>(a.age-b.age))

数组扁平化(2种方法)

  • 数组扁平化就是把多维数组转化成一维数组
  • 方法1: es6提供的新方法 arr.flat(depth) 可将多维数组转换为参数指定的嵌套层数。depth深度级别,可以理解为拆解的层级,默认值为1,即拆一层参数为 Infinity 则无论多少层都(拆掉)转为一维数组
    1
    2
    3
    4
    5
    6
    // 题目
    var data = [1, 2, [3, 4, 5, [6, 7, 8]], 9, 10];
    // 答案
    data.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    // 注意:arr.flat(1)结果是[1, 2, 3, 4, 5, Array(3), 9, 10]
    // arr.flat(2)结果是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • 方法2:for…of遍历数组+cancat/扩展运算符 (注意:扩展运算符只能将一维数组展开,array.push(item1, item2, ..., itemX),push()里可传多值)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 题目
    var data = [1, 2, [3, 4, 5, [6, 7, 8]], 9, 10];
    // 答案
    function flatten(arr) {
    var res = [];
    for(key of arr){
    // 判断数组元素是数组时,递归调用,合并
    if (Array.isArray(key)) {
    res = res.concat(flatten(key)); //concat 并不会改变原数组
    //res.push(...flatten(key)); //或者用扩展运算符
    } else {
    // 判断数组元素不是数组时,直接放入结果数组
    res.push(key);
    }
    }
    return res;
    }
    console.log(flatten(data)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

手写数组的map()

  • array.map(function(currentValue,index,arr), thisValue)
    • arr可选。map方法调用的数组
    • thisValue可选。执行callback函数时被用作this的值。如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。
      1
      2
      3
      4
      // 用法:
      const a = [1, 2, 3, 4];
      const b = array1.map(x => x * 2);
      console.log(b); // Array [2, 4, 6, 8]
  • 数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值
  • map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉
  • 注意: map() 不会对空数组进行检测
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 实现
    Array.prototype.myMap = function (fn, thisValue) {
    let res = []
    // thisValue:执行callback函数时被用作this的值
    thisValue = thisValue || window // 省略 thisValue,或传入 null、undefined,则回调函数的 this 为全局对象
    let arr = this // 得到需要遍历的数组arr
    // 将每个数组元素调用回调函数的结果加入到结果数组中
    for (let i = 0; i < arr.length; i++) {
    // 参数分别为 this指向,当前数组项,当前索引,当前数组
    res.push(fn.call(thisValue, arr[i], i, arr))
    }
    return res
    }

    // 使用 array.myMap(function(currentValue,index,arr), thisValue)
    const a = [1, 2, 3];
    const b = a.myMap((a, index) => {
    console.log(`索引${index}的值为`, a + 1)
    return a + 1;
    }
    )
    console.log(b) // Array 2,3,4
  • 实现步骤:
    1. 定义结果数组res,最后返回它
    2. 解决参数2thisValue空值情况
    3. 通过this得到需要遍历的数组arr
    4. 遍历数组arr,将每个数组元素调用回调函数(参数1fn)的结果加入到结果数组中。
    5. 通过call()让arr中每个数组元素都调用参数1fn,其中this指向为参数2thisValue,需要传入的参数是:当前数组元素,当前索引,当前数组

手写forEach()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现
Array.prototype.myForEach = function (fn, thisValue) {
// thisValue:执行callback函数时被用作this的值
thisValue = thisValue || undefined // 省略 thisValue,则回调函数的 this 为undefined
let arr = this // 得到需要遍历的数组arr
for (let i = 0; i < arr.length; i++) {
// 参数分别为 this指向,当前数组项,当前索引,当前数组
fn.call(thisValue, arr[i], i, arr)
}
}

// 使用 array.myForEach(function(currentValue, index, arr), thisValue)
const a = [1, 2, 3];
a.myForEach((a, index) => {
console.log(`索引${index}的值为`, a + 1)
}
)
// 索引0的值为 2
// 索引1的值为 3
// 索引2的值为 4
  • 实现步骤:
    1. 解决参数2thisValue空值情况
    2. 通过this得到需要遍历的数组arr
    3. 遍历数组arr,通过call()让arr中每个数组元素都调用参数1fn,其中this指向为参数2thisValue,需要传入的参数是:当前数组元素,当前索引,当前数组

手写数组的reduce()【重点】

  • 参考知乎
  • reduce() 方法对数组中的每个元素执行回调函数(参数1)(升序执行),将其结果(函数1返回值)汇总单个返回值
  • array.reduce(callback(total, currentValue, currentIndex, arr), initialValue)
    • 返回计算结果
    • total 必需。初始值initialValue,或 上一次调用回调函数时计算结束后的返回值
    • currentValue 必需。当前元素
    • currentIndex 可选。当前元素的索引
    • arr 可选。当前元素所属的数组对象。
    • initialValue 可选。传递给函数的初始值
  • 注意: reduce() 对于空数组不会执行回调函数
  • 重点:
    • 不是普通的累加,reduce()是返回 每个数组元素执行函数fn的 返回值(即回调函数的参数1) 的累加,所以for循环内要特别注意调用函数fn例子
    • !!不用自己来累加,累加规则是在fn中定义的
1
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
Array.prototype.myReduce = function (fn, initialValue) {
// 1.常规判断
let arr = this;
// 判断是否为空数组,对于空数组不执行回调函数fn
if (arr.length === 0) {
throw new TypeError('empty array');
}
// 判断传入的第一个参数是否为函数
if (typeof fn !== 'function') {
throw new TypeError(`${fn} is not a function`);
}

// 2.初始化各个变量
// 初始化累计结果total
let total = initialValue ? initialValue : arr[0];
// 注意:i的起始值和是否传入参数2 initialValue 有关
let i = initialValue ? 0 : 1;

// 3.循环【重点】
// 返回累计结果total
for (i; i < arr.length; i++) {
// 注意:reduce做的不是普通的累加,是 每个数组元素执行函数fn的 返回值 的累加,total既是函数返回值也是累计
total = fn(total, arr[i], i, arr);
// !不用自己来累加,累加规则是在fn中定义的
}
// 4.返回累计结果total
return total;
}

// 使用
let arr = [4, 8, 15, 16, 23, 42];
let add = function (a, b) {
return a + b;
}
let sum = arr.myReduce(add, 10);
console.log(sum); // 118
  • 思路:
    1. 常规判断:主要要判断的是调用数组、传入参数
    2. 初始化各个变量,为第一次执行函数做准备,这里我们需要准备的变量有需要传入回调函数的4个参数accumulator, currentValue, currentIndex, sourceArray
    3. 开始循环 同时记得更新几个参数和返回结果
    4. 返回结果

手写通用深拷贝

  • 重点
    • for…in 遍历 对象属性/数组元素,key是属性名/数组下标。
    • for…in会遍历到对象/数组的继承属性,注意筛选自身属性
    • 注意:是Array.isArray(obj),Array开头
    • 注意:typeof obj !== "object"有引号
    • 注意:筛选自身属性使用的是obj.hasOwnProperty(key),obj开头
  • 可参考“JS变量类型和计算”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 深拷贝方法1
    * @param {Object} obj 要拷贝的对象
    */
    function deepClone(obj) {
    // 如遇值类型则直接返回,否则继续执行【注意Object有引号】
    if (typeof obj !== "object" || obj === null) {
    return obj;
    }

    // 初始化返回结果
    let cloneObj = Array.isArray(obj) ? [] : {};

    // 遍历对象属性/数组元素
    for (key in obj) {
    // 筛选对象自身属性
    if (obj.hasOwnProperty(key)) {
    // 递归调用deepClone(),
    cloneObj[key] = deepClone(obj[key])
    }
    }

    // 返回结果
    return cloneObj;
    }
1
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
/**
* 深拷贝方法2
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj){
// 根据传入的参数obj判断拷贝数组还是对象
let objClone = Array.isArray(obj)?[]:{};
// 判断参数obj是否是引用类型(数组/对象)【注意Object有引号】
if(obj && typeof obj==="object"){
// 是对象则用for in遍历 对象属性
for(key in obj){
// 如果是非继承属性
if(obj.hasOwnProperty(key)){
//判断ojb子元素(数组元素/对象属性)是否为引用类型(数组、对象、函数)
if(obj[key]&&typeof obj[key] ==="object"){
//是则递归复制
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }],
b=deepClone(a);
b[3][0] = 5;
console.log(a,b);

读代码写输出结果

  • 注意:当出现多个同名变量与同名函数时,调用该变量名时的优先级为:变量声明< 函数声明 < 变量赋值
  • 原型和实例上属性相同,优先从实例上查找
1
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
function Foo() {
getName = function () { console.log (1); };
return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}

// 相当于
function getName() { console.log (5);} // 函数提升
function Foo() {
getName = function () { console.log (1); };
return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};

//请写出以下输出结果:
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); //2
new Foo().getName(); // 3
new new Foo().getName();// 3
  1. Foo.getName(); //2 获取Foo对象的getName属性,原型和实例上属性相同,优先从实例上查找
  2. 【重点】getName(); //4 因为直接调用getName,所以排除1,2,3。 4和5有变量声明提升和函数声明提升 两个坑,函数声明整个函数提升,所以5被提升到4上面。使用函数表达式声明的函数不存在函数提升。所以最后是4。
  3. 【重点】Foo().getName(); //1 执行Foo()返回的this指向window,此时的Foo().getName()相当于window.getName(),则由1、4、5可选,由于该函数第一行getName = function () { alert (1); };没有用var声明,所以是操作的是全局变量,因此修改了先前的getName()函数(5、4)【如果1中有var则最终结果是4】
  4. 【重点】getName(); //1 同上window.getName()【如果1中有var则最终结果是4】
  5. new Foo.getName(); //2 考察js运算符优先级 这里的new操作符是无参数的所以优先级低,实际执行结果new ((Foo.getName)())
  6. 【重点】new Foo().getName(); //3 实际相当于(new Foo()).getName(),隐式原型没有就去构造函数的显式原型找,于是找到3
  7. 【重点】new new Foo().getName(); //3先初始化Foo的实例化对象,1没绑定在this上,所以会一路找到显式原型(3),将其原型上的getName作为构造函数再次new,还是3

运算符优先级


简述call,apply,bind区别

  • 共同点:
    • 都是用来改变函数的this对象的指向的。
    • 第一个参数都是this要指向的对象
    • 都可以利用后续参数传参
  • call跟apply的用法几乎一样,唯一的不同就是传递的参数不同。
    • call只能一个参数一个参数的传入。
    • apply则只支持传入一个数组,哪怕是一个参数也要是数组形式。最终调用函数时候这个数组会拆成一个个参数分别传入。
  • bind方法,他是直接改变这个函数的this指向并且返回一个新的函数,之后再次调用这个函数的时候this都是指向bind绑定的第一个参数。bind传参方式可以跟call方法一致
    1
    2
    3
    4
    5
    // 传参方式对比
    xw.say.call(xh,"实验小学","六年级");
    xw.say.apply(xh,["实验小学","六年级"]);
    xw.say.bind(xh,"实验小学","六年级")();
    xw.say.bind(xh)("实验小学","六年级"); // 由于bind返回的仍然是一个函数,所以我们还可以在调用的时候再进行传参。

手写call()

  • this指向的理解
  • ES6 参数默认值
  • delete 操作符用于删除对象的某个属性
  • 注意:虽然call()不是返回新的函数,但f.call(obj, 1, 2) 中执行函数f所返回的东西就是最终返回的,所以手动实现时要注意返回值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 函数的方法,所以写在Fuction原型对象上
    Function.prototype.myCall = function (context = window) { // ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
    // 传入的参数1 context 设置为this,即现在调用方法的对象
    const obj = context;
    // this为原本调用的上下文,this此处为函数(即f.myCall()中的f),将这个函数作为obj的方法,让传入的this(即obj)来调用这个方法
    obj.fn = this;
    //第一个为obj所以删除,伪数组转为数组
    const args = [...arguments].slice(1)
    // 注意:执行函数f所返回的东西就是最终返回的
    let res = obj.fn(...args);
    // 不删除会导致context属性越来越多
    delete obj.fn;
    // 注意:这里把obj.fn(...args)返回,调用myCall()就相当于调用obj.fn(...args)
    return res;
    }

    //用法:f.myCall(obj,arg1)
    function f(a, b) {
    console.log(a + b)
    console.log(this.name)
    }
    let obj = {
    name: 1
    }
    f.myCall(obj, 1, 2) // 3,1 原本this指向window

手写apply()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 函数的方法,所以写在Fuction原型对象上
// 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
Function.prototype.myApply = function (context = window) { // ES6方法,为参数添加默认值
let obj = context // 传入的参数1 context 设置为this,即现在调用方法的对象
obj.fn = this // this为原本调用的上下文,this此处为函数(即f.myCall()中的f),将这个函数作为obj的方法,让传入的this(即obj)来调用这个方法
const arg = arguments[1] || [] // 若有参数,得到的是数组
let res = obj.fn(...arg)
delete obj.fn
return res
}

//用法:f.myApply(obj,[arg1,...])
function f(a, b) {
console.log(a, b)
console.log(this.name)
}
let obj = {
name: '张三'
}
f.myApply(obj, [1, 2]) // 1,2 张三

手写bind()

  • 返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
  • 剩余参数...是做聚合的,它会将那些没有对应形参的实参们聚合成一个数组。而扩展运算符...是做展开的,符号都是...,但含义不同。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // fn.bind(obj,arg1)()
    // 函数的方法,所以写在Fuction原型对象上
    Function.prototype.myBind = function (context = window) { // ES6方法,为参数添加默认值
    // 传入的参数1 context 设置为this,即现在调用方法的对象
    let obj = context
    // this为f.myBind(...) 中的 f
    obj.fn = this
    let args = [...arguments].slice(1);
    // 和call、apply不同的是bind返回的是一个新的函数
    return function () {
    return obj.fn(...args);
    }
    }

    // 用法:f.myBind(obj,arg1)()
    function f(a, b) {
    console.log(a + b)
    console.log(this.name)
    }
    let obj = {
    name: "李四"
    }
    f.myBind(obj, 1, 2)() // 3 李四

instanceof【重点复习】

  • 注意:[] instanceof Array[] instanceof Object都是true
  • break 语句可用于跳出循环,break 语句跳出循环后,会继续执行该循环之后的代码(如果有的话),只能用在循环或 switch 中。
  • 考察对**原型链**的理解
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 引用类型(对象、数组、函数) instanceof 构造函数
  • 作用:测试它 左边的引用类型(对象、数组、函数)实例对象 是否是它 右边的构造函数(Object()、Array()、Funtion())的实例
  • 思路:
    • 如果左边为基本数据类型,就直接返回false
    • instanceof 在查找的过程中会遍历左边实例对象的原型链,直到找到右边变量的 prototype才返回true,查找失败返回 false
    • 首先 instanceof 左侧必须是对象, 才能找到它的原型链
    • instanceof 右侧必须是函数, 函数才会prototype属性
    • 迭代 , 左侧对象的原型不等于右侧的 prototype时, 沿着原型链重新赋值左侧
  • 注意:
    • **Object.prototype.__proto__ === null; // true**。
    • 结束while循环可使用无条件跳出语句break;函数返回语句return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// [1,2,3] instanceof Array ---- true

// L instanceof R
// 函数R的显式原型 存在于 变量L的原型链上
function instance_of(L,R){
// 验证如果为基本数据类型,就直接返回false
if (typeof L !== "object" || L === null) {
return false;
}

let RP = R.prototype; //取 R 的显示原型
L = L.__proto__; //取 L 的隐式原型
while(true){ // 无线循环的写法(也可以使 for(;;) )
if(L === null){ //找到最顶层,即L为 构造函数.prototype 时还没找到RP则false
return false; // return会结束while循环
}
if(L === RP){ //严格相等
return true;
}
L = L.__proto__; //没找到则继续向上一层原型链查找
}
}

// 使用
console.log(instance_of([1, 2, 3], Array)); // true

new【重点复习】

  • 创建对象实例过程的理解
  • new一个对象的步骤:
    1. 创建一个新(空)对象obj
    2. 空对象的隐式原型__proto__指向构造函数的显式原型prototype
    3. 使用apply()改变this指向,让构造函数的this指向新对象obj,让obj调用构造函数fn,为实例obj添加方法和属性
    4. 确保返回的是一个对象,如构造函数fn无返回值或返回一个非对象值,则将 obj 返回作为新对象;如返回值是新对象则直接返回该对象。
      1
      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
      // 构造函数Person
      function Person(name, age) {
      this.name = name
      this.age = age
      }
      Person.prototype.sayHi = function () {
      console.log('Hi!我是' + this.name)
      }
      let p1 = new Person('张三', 18)
      p1.sayHi() // Hi!我是张三

      // 手动实现new
      function create() {
      // 创建新对象obj
      let obj = {}
      // 获取构造函数fn
      let fn = [...arguments].shift() //将arguments对象转化为数组,删除arguments数组的第一个元素
      // 将新对象的隐式原型指向构造函数的 显式原型
      obj.__proto__ = fn.prototype
      // 使用apply()改变this指向新对象obj,让obj调用构造函数fn,为实例obj添加方法和属性
      let res = fn.apply(obj, arguments)
      // 确保返回的是一个对象,如构造函数fn无返回值或返回一个非对象值,则将 obj 返回作为新对象;如返回值是新对象则直接返回该对象
      return typeof res === 'object' ? res : obj
      }

      let p2 = create(Person, '李四', 19)
      p2.sayHi() // Hi!我是李四

防抖和节流

  • 区别: 防抖是在触发结束 n秒 都没触发了才会执行事件函数,节流则是只要连续触发,每过n秒就执行一次事件函数,不会因为连续触发就重新计算n秒起始位置。
  • 思路区别:
    • 都是使用定时器setTimeout定时器id(timer)
    • 防抖需要清除定时器clearTimeout(timer)),当存在 定时器id(timer) 时清除定时器,以此保证 之前设置的延迟事件 在 当前延迟时间内 再次触发时,之前的延迟事件 不会被执行节流则不需要清除定时器
    • 相对的,节流是当存在 定时器id(timer) 时,直接退出事件函数return),避免在 当前延迟时间内 产生新的定时器,以此保证每一段延迟时间内只触发一次事件函数
    • 其他都一样。
  • 注意:封装 可复用函数debounce()/throttle()时,在函数内要返回函数!否则调用debounce()/throttle()时没有函数可调用。具体原因可看“JS 运行环境”中“节流”在 普通事件函数中 的代码和 封装 后的代码区别,不对函数进行封装则不需要返回函数。
    • 【切记!】对xx封装就要返回xx,这里debounce()/throttle()都是对 函数(原本的事件函数) 进行封装,所以他们都需要返回被封装的函数(即原本的事件函数),否则在绑定事件函数时,throttle()就变成直接调用函数了,而这里需要的是事件函数。

防抖【重点复习】【函数内返回函数,才能调用 debounce(),对比“JS 运行环境”中“节流”在 普通事件函数中 的代码和 封装 后的代码区别,思考为啥】

  • 推理过程可参考“JS 运行环境”
  • 防抖:指触发事件n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则以最后一次触发为标准重新计算函数执行时间
    • 与节流的区别防抖是在触发结束 n秒 都没触发了才会执行事件函数,节流则是只要连续触发n秒就执行事件函数,不会因为连续触发就重新计算n秒起始位置。
  • (连续不断触发时不调用,触发完后过一段时间调用)
  • 比如,监听一个输入框的文字变化后触发change事件,直接用keyup事件,则会频发触发change事件。如果使用防抖,则连续不断输入时,不会触发change事件;当一段时间内不输入了,才会触发change事件;如果小于这段时间继续输入的话,时间会重新计算,也不会触发change事件。(防抖不仅限于change事件)
  • 实际上是对定时器setTimeout的使用,但我们通常将其封装为debounce函数来使用
  • 代码思路:
    • debounce函数,参数1:事件函数,参数2:防抖时间(delay毫秒)
    • debounce函数内,使用 定时器setTimeout() 延迟(delay毫秒)执行事件函数
    • 每次触发事件都会生成定时器id(timer),如之前已存在定时器id(timer),则删除之前的定时器clearTimeout(timer)),重新产生 新定时器 以及 新的timer ,并在新定时器内 清除上一个timer(timer=null)
      1
      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
      <input type="text" id="input1">
      <script type="text/javascript">
      function debounce(fn, delay = 500) {
      if (typeof fn !== 'function') {
      throw new TypeError('fn不是函数')
      }
      // 创建timer存储定时器id
      let timer = null;
      // 返回的函数就是debounce函数,下面调用debounce()时传入的参数1(箭头函数)和参数2(delay)实际都传到这个返回函数中。只有返回函数,才能调用 debounce()
      // 需要被封装的 事件函数 ,所以封装函数内要返回函数
      return function () {
      var args = [...arguments];
      if (timer) { // 一开始为空,则不执行清空
      clearTimeout(timer);
      }
      // 回调函数内this指向默认window,使用箭头函数获取我们希望的this指向
      timer = setTimeout(() => { // setTimeout()返回定时器id
      // 用apply指向调用debounce的对象,防止fn有this或者参数(arguments对象,包括e)需要传入debounce函数
      fn.apply(this, args);
      }, delay);
      };
      }

      // 调用
      const input1 = document.getElementById("input1");
      // debounce()必须返回函数,否则debounce()就变成直接调用函数了,而这里需要的是事件函数
      input1.addEventListener('keyup', debounce(() => {
      // 一般为发送ajax请求
      console.log(input1.value)
      }), 600)
      </script>
  • 案例解释
    1. timer用于存储定时器id,输入a时,通过setTimeout给timer赋值,准备500毫秒后触发打印
    2. 紧接着输入s时,由于此时timer不为空,所以执行clearTimerout将之前的定时器清除,重新创建setTimeout给timer赋值,准备500毫秒后触发打印
    3. 依旧紧接着输入d时,一直到s都重复上一步逻辑
    4. 直到输入最后一个f时,清除了上一个timer后创建了新的timer,且500毫秒后未被清除,故打印asdfasdfasf,并将timer清空

节流【重点复习】

  • 节流连续触发事件但是在 n 秒中只执行一次函数
    • 与防抖的区别防抖是在触发结束 n秒 都没触发了才会执行事件函数,节流则是只要连续触发n秒就执行事件函数,不会因为连续触发就重新计算n秒起始位置。
  • 比如:(连续不断动都需要调用时用,设一时间间隔),拖拽一个元素时,要随时拿到该元素被拖拽的位置。这里直接用drag事件则会频发触发,很容易导致卡顿。而使用节流,则无论拖拽速度多快,都会每隔100ms触发一次(时间自定义)
    • 注意:如果用防抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
    • 应当应用节流的拖拽元素,调用防抖停止拖拽时才会执行事件函数
  • 代码思路:
    • throttle函数,参数1:事件函数,参数2:节流时间(delay毫秒)
    • throttle函数内,使用 定时器setTimeout() 延迟(delay毫秒)执行事件函数,生成定时器id(timer)
    • 每次触发事件都会生成定时器id(timer),如之前已存在定时器id(timer),则直接退出事件函数return),直到事件函数执行完成后删除当前timer才允许再次通过定时器设置新的timer。以此保证delay毫秒内只产生1个timer,当这次事件函数执行完成后清除当前timer(timer=null),
      1
      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
      <!-- draggable设置为可拖拽元素 -->
      <div id="div1" draggable="true">可拖拽</div>

      <script type="text/javascript">
      // 使用节流,则无论拖拽速度多快,都会每隔100ms触发一次(时间自定义)
      function throttle(fn, delay = 100) {
      // timer在闭包内
      let timer = null;
      // 返回的函数就是throttle函数,下面调用throttle()时传入的参数1(箭头函数)和参数2(delay)实际都传到这个返回函数中。只有返回函数,才能调用throttle()
      // 需要被封装的 事件函数 ,所以封装函数内要返回函数
      return function () {
      var args = [...arguments];
      if (timer) {
      return;
      }
      timer = setTimeout(() => {
      // 这里如果用fn(),则e无法传入
      fn.apply(this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
      // fn.apply(this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
      timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
      }, delay)
      }
      }

      const div1 = document.getElementById('div1');
      // throttle()必须返回函数,否则throttle()就变成直接调用函数了,而这里需要的是事件函数
      div1.addEventListener('drag', throttle((e) => {
      // 相对于定位父盒子的x,y坐标
      console.log(e.offsetX, e.offsetY)
      }, 500))
      </script>
  • e和apply()的关系可见“JS 运行环境”的“节流”最后的图示和注意
  • 案例解释:
    1. 和节流相似,都设置一个定时器,初始为空
    2. 元素开始拖拽时触发事件,根据setTimeout返回的id赋给timer,此时需要等待100毫秒才打印位置
    3. 由于快速拖动元素,100毫秒内再次出发drag事件,但此时timer有值,直接退出drag事件,直到100毫秒位置打印后timer清空才会再走第二步。以此实现无论拖拽速度多快,都会每隔100ms触发一次drag事件

手写ajax

  • 下面是get请求,post请求的实现可看“JS Web API AJAX”
  • 步骤:
    1. 创建 XMLHttpRequest 实例
    2. 发出 HTTP 请求
    3. 服务器返回 JSON 格式的字符串
    4. JS 解析 JSON,并更新局部页面
  • 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。(responseText 获得字符串形式的响应数据,responseXML 获得 XML 形式的响应数据)
  • 手写最简单的 GET 请求:
    1
    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 ajax() {
    // 创建 XMLHttpRequest 实例,以调用方法
    let xhr = new XMLHttpRequest()
    // 创建 HTTP 请求。参数2,url。参数三:异步
    xhr.open('get', 'https://www.google.com')
    // 每当 readyState 属性改变时,就会调用onreadystatechange
    xhr.onreadystatechange = () => {
    // 必须在readyState属性值为4时才能去判断status属性,否则拿不到status属性值
    if (xhr.readyState === 4) {
    // 200-300请求成功
    if (xhr.status >= 200 && xhr.status < 300) {
    // responseText获得字符串形式的响应数据
    let string = xhr.responseText
    // JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
    let object = JSON.parse(string)
    }
    }
    }
    xhr.send() // 用于实际发出 HTTP 请求。不带参数为GET请求
    }

    // 使用
    myButton.addEventListener('click', function () {
    ajax()
    })
  • 封装为promise:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function ajax(url) {
    const p = new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open('get', url)
    xhr.onreadystatechange = () => {
    if (xhr.readyState == 4) {
    if (xhr.status >= 200 && xhr.status <= 300) {
    resolve(JSON.parse(xhr.responseText))
    } else {
    reject('请求出错')
    }
    }
    }
    xhr.send() //发送hppt请求
    })
    return p
    }

    // 使用
    let url = '/data.json'
    ajax(url).then(res => console.log(res))
    .catch(reason => console.log(reason))

用 setTimeout 实现 setInterval


图片懒加载

  • 可参考“JS 运行环境”中“懒加载”
    懒加载图示
  • 思路:
    1. html内自定义属性data-realsrc存储图片真实srcsrc内放假图的src
    2. 根据 DOM元素距离顶部的值(img.offsetTop)<视口高度(document.documentElement.clientHeight)+页面顶部垂直滚动到视口顶部的距离(document.documentElement.scrollTop) 判断是否滑动到图片位置
    3. 等**滑动到图片位置时再将data-realsrc赋值给src**,加载真实图片
1
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
<!-- 一开始加载src地址下的图片,等滑动到下一图片位置时将data-realsrc的属性值赋给src,此时即为加载真正的图片 -->
<img id="img1" src="./上校2.jpg" data-realsrc="../草莓头像.png" />

<script type="text/javascript">
// 根据DOM元素距离顶部的值来计算图片位置
function lazy() {
// 获取图片列表,即img标签列表
let imgs = document.getElementsByTagName('img');
// 视口高度
let viewHeight = document.documentElement.clientHeight; // clientHeight是元素height + padding - 水平滚动条高度 (如果存在)
// 图片滚动过的高度(页面顶部滚动到视口顶部的距离)
let scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; // scrollTop元素内容顶部到视口可见内容顶部的距离
for (let i = 0; i < imgs.length; i++) {
// offsetTop当前元素相对于 body 元素的顶部内边距的距离
let offsetHeight = imgs[i].offsetTop;
// 滑动到图片位置时再将data-realsrc赋值给src,加载真实图片
// 图片顶部到body顶部距离<视口高度+页面顶部垂直滚动到视口顶部的距离
if (offsetHeight < viewHeight + scrollHeight) { // 说明滑动到图片位置了
// 使用dataset.xxx/getAttribute("data-xxx")获取自定义属性
imgs[i].src = imgs[i].dataset.realsrc;
}
}
}

// onload是等所有的资源文件加载完毕以后再绑定事件
window.onload = function() {
lazy(imgs);
}

// 滚屏函数
window.onscroll = function() {
lazy(imgs);
}
</script>
  • Element.clientHeight(只读属性)它是元素内部的高度,CSS height + CSS padding - 水平滚动条高度 (如果存在)
    • Window.innerHeight浏览器窗口的视口(viewport)高度(以像素为单位),与clientHeight区别是如果有水平滚动条,innerHeight也包括滚动条高度。
  • Element.scrollTop获取或设置一个元素的内容垂直滚动的像素数。即,这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。
    • document.documentElement根元素(HTML文档就是HTML元素)
    • 根元素的scrollTop即页面顶部垂直滚动到视口顶部的距离
  • HTMLElement.offsetHeight(只读属性)它返回该元素的像素高度,高度**包含该元素的垂直内边距(padding)和边框(border)**,且是一个整数。
  • HTMLElement.offsetTop(只读属性)它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离
    • offsetParent最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table,td,th,body元素

手写Promise加载2张图片

  • 可参考“JS基础知识面试题(2)”中“Promise加载2张图片(解决回调地域)”
    1
    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
    // 加载2张图片,即使用Promise解决回调地域
    function loadImg(src) {
    const p = new Promise(
    (resolve, reject) => {
    const img = document.createElement('img')
    img.onload = () => {
    resolve(img)
    }
    img.onerror = () => {
    const err = new Error(`图片加载失败 ${src}`)
    reject(err)
    }
    // 注意这里动态加载图片
    img.src = src
    }
    )
    return p
    }

    const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
    const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'

    loadImg(url1).then(img1 => {
    console.log(img1.width)
    return img1 // 普通对象
    }).then(img1 => {
    console.log(img1.height)
    return loadImg(url2) // promise 实例
    }).then(img2 => {
    console.log(img2.width)
    return img2
    }).then(img2 => {
    console.log(img2.height)
    }).catch(ex => console.error(ex))

async/await加载2张图片

  • 可参考“JS基础知识面试题(2)”中“async/await彻底解决回调地狱”
  • Promise then catch链式调用可解决回调地狱,但也是基于回调函数
  • 彻底解决回调地狱,用同步的方式编写异步
    1
    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
    // 依旧是用loadImg()返回Promise实例
    function loadImg(src) {
    const promise = new Promise((resolve, reject) => {
    const img = document.createElement('img')
    img.onload = () => {
    resolve(img)
    }
    img.onerror = () => {
    reject(new Error(`图片加载失败 ${src}`))
    }
    img.src = src
    })
    return promise
    }

    // 注意:await 必须放在 async 函数中,否则会报错
    (async function () {
    const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
    const src2 = 'https://avatars3.githubusercontent.com/u/9583120'

    try {
    // 加载第一张图片
    const img1 = await loadImg(src1)
    console.log(img1.width, img1.height) // 252,144
    // 加载第二张图片
    const img2 = await loadImg(src2)
    console.log(img2)
    // <img src="https://avatars3.githubusercontent.com/u/9583120">
    } catch (ex) {
    console.error(ex)
    }
    })()

创建10个a标签,点击的时候弹出来对应的序号

  • 可参考“JS基础知识面试题(1)”中“创建10个a标签,点击时弹出对应的序号”

  • 方法1:let

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let Fragment = document.createDocumentFragment();
    // 循环创建10个a标签
    for (let i = 1; i <= 10; i++) {
    let a = document.createElement('a');
    a.innerHTML = i + `<br/>`;
    // 绑定事件函数
    a.addEventListener('click', (e) => {
    e.preventDefault();
    alert(i);
    })
    // 统一添加到Fragment中
    Fragment.appendChild(a);
    }
    // Fragment添加到文档中
    document.body.appendChild(Fragment);
  • 注意:

  • 方法2:使用闭包的持久性,注意重点不在匿名自执行函数,将i传进函数中作为 函数作用域 的变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var Fragment = document.createDocumentFragment();
    for (var i = 1; i <= 10; i++) {
    // 注意:光是设置一个函数包裹,这个函数是不会执行的,需要自调用
    ((i) => {
    var a = document.createElement('a');
    a.innerHTML = i + `<br/>`;
    a.addEventListener('click', (e) => {
    e.preventDefault();
    alert(i);
    })
    Fragment.appendChild(a);
    })(i)
    }
    document.body.appendChild(Fragment);
  • 注意:for循环中光是设置一个箭头函数,这个函数是不会执行的,需要自调用


函数实现一秒钟输出一个数

  • 方法1:使用let块级作用域
    1
    2
    3
    4
    5
    for(let i=0;i<=10;i++){   //用var打印的都是11
    setTimeout(()=>{
    console.log(i);
    },1000*i)
    }
  • 方法2:不用let的写法,原理是用 立即执行函数 创造一个 函数作用域 来 保存变量i
    1
    2
    3
    4
    5
    6
    7
    for(var i = 1; i <= 10; i++){
    (function (i) {
    setTimeout(function () {
    console.log(i);
    }, 1000 * i)
    })(i);
    }

手写图片轮播

  • 图示、完整的各个步骤流程等可看“前端面试题目(1)”
  • 【大坑】:不要在css设置left,js获取会很麻烦,parseFloat()都转不成数字,只能得到NAN,去JS(copyPic()中)设置添加辅助图后的left来展示第一张图片
  • JS全思路:
    • 使用ES6 class很重要!创建一个class Slider,这个类的作用就是获取轮播图的父盒子id然后给他内部的轮播图、小圆点、左右按钮添加js逻辑。在需要使用轮播图的文件中使用Slider类来new一个对象,同时将Slider需要的DOM元素(轮播图的父盒子)的id传过去,相对应的逻辑就绑上去了
    • 在类的定义中,动态添加圆点(initPoint()
    • ul头尾增加头尾辅助图(copyPic()),每次移动后判断图片位置(ul绝对定位的left),头/尾辅助图处于显示框中时通过设置ul的left迅速整条移动替换为期望的 真头图/尾图 位置,实现**图片无限滚动copyPic()添加首尾辅助图,move()将辅助图位置替换为真实对应图片的位置**
      • 【重点!比如:ul到首图前的辅助图(假尾图)时,要设置left负数,整体左移到尾图】
    • 触发轮播图时的移动效果统一提取到函数move()点击左右按钮/点击圆点时调用 move()
    • leftRight()实现左右按钮点击跳转,左右点击时控制 当前小圆点索引this.index的值的加减,方便move(offset)控制圆点样式的变化animate()实现带动画的移动
    • play() 自动播放,即setInterval()定时器点击右边按钮mouseenter时删除定时器使鼠标移入时停止继续播放,mouseleave时重新创建定时器(保存在this.auto中,方便下次清除定时器)
    • 点击圆点跳转图片: initPoint()中添加,根据图片属性data-index和当前圆点索引this.index计算所需位移offset,调用move()进行跳转
      • 跳转前记得改 当前小圆点索引this.index,以此方便move()中获取index改变小圆点样式
      • 注意下,圆点绑定click事件中e.target是当前的DOM元素
  • HTML重点:
    • 单张图放入li中,所有图合为ul,ul外用div做单张图大小的div框(slider)。
    • 箭头写在span中,放在div(left-box)中。
    • 小圆点都放li中,ol为一排圆点,div(index-box)包裹ol。
  • CSS重点:
    • 总结:显示的图片框(相对定位)内部各个块(绝对定位)通过子绝父相来进行定位,每个小块内部通过flex布局来进行(箭头)居中/(小圆点)均匀分布。图片条ul(绝对定位),通过left的更改进行滑动。
    • li设为float,使所有li并排单个移动时跟随兄弟元素一起移动。(ul的父元素slider(div)宽度为单张图片宽度,**ul宽度设为500%**)
    • 子绝父相(子为ul元素,父为显示在页面上的ul的父元素slider(div)),注意ul的父元素slider(div)使用overflow:hidden隐藏横向滚动条,图片浮动为一排通过left移动
    • 绝对定位使箭头(left-box)处于图片 框的左/右位置,left-box内flex布局使箭头在left-box内居中显示:
      • 箭头(span)的父元素(left-box)设为flex,轻松使箭头在父元素(left-box)中居中显示。箭头的父元素(left-box)absolute,slider框relative,箭头的父元素(left-box)在轮播图框(slider)中靠左显示。右箭头同理。
    • ol的父元素(index-box)通过绝对定位固定在slider(div)的底部位置,小圆点(li)的父元素ol设为flex,默认把子项排在同一行。justify-content: space-evenly;使视觉上看任意两个子项之间的间距(以及到边缘的间距)相等space-around视觉上看不均匀)。li设置cursor: pointer;,使用list-style: none;取消列表项目符号
  • JS部分重点:
    • 创建Slider类(ES6 class): 在该类构造函数中接收轮播图的父盒子id,以便接下来在类中给他内部的轮播图、小圆点、左右按钮添加js逻辑
    • 使用Slider类: 在需要使用轮播图的文件中使用Slider类来new一个对象,同时将Slider需要的DOM元素(轮播图的父盒子)的id传进去相对应的逻辑就绑上去了
      • 这样可以在js中通过同一个类创建多个不同的轮播图对象
    • 类的 构造函数中 要调用class中自定义的相关函数(比如动态添加小圆点、自动播放的函数等待)!因为创建对象时会执行构造函数,所以在构造函数内调用的函数将会被执行。
    • 注意:类中自定义函数不需要function。需要频繁调用的DOM元素可以放到构造函数中一次性获取后挂在this上,后面通过this.xxx调用DOM元素。
    • 无限滚动的实现: ul首尾加辅助图,通过ul的left判断目前是否处于辅助图位置,如抵达辅助图位置,则通过改变ul的left使其迅速移动到辅助图对应的真实图片位置上
  • CSS:
    1
    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
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    <style type="text/css">
    img {
    width: 500px;
    }

    .slider-box {
    margin-right: 10px;
    }

    /*slider做轮播图显示框,与单张图等宽高,5张图并排,overflow隐藏溢出部分*/
    .slider-box .slider {
    position: relative;
    width: 500px;
    height: 500px;
    overflow: hidden;
    }

    /*ul为全部图组成的横排,从第2张图开始显示*/
    .slider-box .slider ul {
    /* 去掉ul自带的padding */
    padding: 0;
    /* absolute可产生BFC,不用另外清除浮动 */
    position: absolute;
    top: 0px;
    /* 【不要在css设置left,js获取会NAN,去JS设置添加辅助图后的left】无限滚动 */
    /* left: -500px; */
    width: 500%;
    /*所有图片的宽度和*/
    height: 100%;
    /* transition: left 1s linear; */
    }

    /*每张图都放在1个li里,横排显示*/
    .slider-box .slider ul li {
    float: left;
    /*浮动以显示在一排,且父元素left移动时会全部一起动*/
    }

    /* 左箭头 */
    .slider-box .left-box {
    background: rgba(0, 0, 0, 0.2);
    position: absolute;
    top: 45%;
    left: 0px;
    width: 50px;
    height: 50px;
    color: #fff;
    display: flex;
    /*flex容器可使子元素轻松居中*/
    justify-content: center;
    /*水平居中*/
    align-items: center;
    /*垂直居中*/
    cursor: pointer;
    }

    /*右箭头*/
    .slider-box .right-box {
    background: rgba(0, 0, 0, 0.2);
    position: absolute;
    top: 45%;
    right: 0px;
    width: 50px;
    height: 50px;
    color: #fff;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    }

    .slider-box .left-box,
    .slider-box .right-box span {
    font-size: 24px;
    }

    /*小圆点索引*/
    .slider-box .index-box {
    position: absolute;
    bottom: 1%;
    left: 50%;
    transform: translateX(-50%);
    }

    .slider-box .index-box>ol {
    padding: 0;
    height: 14px;
    /* flex默认把子项排在同一行 */
    display: flex;
    /* evenly:均匀地。任意两个子项之间的间距(以及到边缘的间距)相等 */
    justify-content: space-evenly;
    align-items: center;
    background: rgba(0, 0, 0, 0.6);
    }

    .slider-box .index-box>ol>li {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #fff;
    /* list-style取消列表项目符号 */
    list-style: none;
    cursor: pointer;
    }

    /* 当前图片的圆点样式 */
    .slider-box .index-box>ol>li.active {
    background: #e1251b;
    }
    </style>
  • HTML:
    1
    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
    <div class="slider-box">
    <!-- slider做一个框,和单张图等宽等高,overflow隐藏溢出部分 -->
    <!-- 子绝父相,slider相对定位,ul绝对定位,调整ul的left使其移动-->
    <div class="slider" id="slider">
    <!-- ul宽是所有图的宽,内部li浮动来一排显示-->
    <ul>
    <li>
    <img src="../../醒醒头像.jpg" alt="">
    </li>
    <li>
    <img src="../../草莓头像.png" alt="">
    </li>
    <li>
    <img src="../../胡萝卜头像.jpg" alt="">
    </li>
    </ul>

    <!-- 左箭头,绝对定位,left-box设置为flex容器,实现span水平垂直居中-->
    <div class="left-box">
    <span>&lt;</span>
    </div>
    <!-- 右箭头,同上中-->
    <div class="right-box">
    <span>&gt;</span>
    </div>

    <!-- 小圆点索引,绝对定位,通过js根据图片数量决定圆点数量-->
    <div class="index-box">
    <ol>
    </ol>
    </div>
    </div>
    </div>
  • JS:
    1
    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
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    <script type="text/javascript">
    class Slider {

    constructor(id) {
    // 需要频繁调用的DOM元素可在构造函数中一次性获取
    this.box = document.querySelector(id) // 轮播图盒子
    this.picBox = this.box.querySelector("ul") // 图片盒子
    this.indexBox = this.box.querySelector(".index-box") // 小圆点父盒子
    // clientWidth元素内部宽度,包括 padding,不包括 border、 margin 和垂直滚动条(如果有的话)
    this.sliderWidth = this.box.clientWidth // 整个轮播图的宽度sliderWidth
    this.sliders = this.picBox.children.length; // 轮播图数量,这里是3,还没加首尾辅助图
    this.index = 1 // 小圆点的索引,初始为1
    this.auto = null

    this.init()
    }

    init() {
    this.initPoint() // 动态添加小圆点,点击圆点跳转对应图片
    this.copyPic() // 增加辅助图,实现无限滚动
    this.leftRight() // 左右按钮
    this.play() // 自动播放
    }

    // 动态添加小圆点
    initPoint() {
    // 图片的数量就是圆点的数量
    const num = this.picBox.children.length;
    // 减少DOM操作(Fragment)
    let frg = document.createDocumentFragment();

    for (let i = 0; i < num; i++) {
    let li = document.createElement("li")
    // 提前给圆点设置属性data-index,用于点击跳转
    li.setAttribute("data-index", i + 1)
    // 给当前图的小圆点特殊样式,默认第一张图
    if (i == 0) li.className = "active"
    frg.appendChild(li)
    }

    // 动态设置 圆点框 的总宽度,设置的是indexBox的 子元素ol 的宽度
    this.indexBox.children[0].style.width = num * 10 * 2 + "px";
    // 一次性添加到小圆点父盒子中
    this.indexBox.children[0].appendChild(frg)

    // 点击圆点,跳转对应图片
    this.indexBox.children[0].addEventListener("click", (e) => {
    console.log("point")
    // 获取当前点击的DOM元素的data-index属性值
    let pointIndex = (e.target).getAttribute("data-index")
    // index是构造函数中定义并初始为1的当前小圆点索引
    if (pointIndex == this.index) {
    return
    }

    // 跳转相应图片,offset为移动距离
    // index为当前小圆点索引(构造函数中定义初始为1)
    // sliderWidth为轮播图的宽度
    let offset = (pointIndex - this.index) * this.sliderWidth
    // 跳转前记得改当前小圆点索引index,以此方便move()中获取index改变小圆点样式
    this.index = pointIndex
    this.move(offset) // 调用move()移动图片
    })
    }

    // ul头尾增加辅助图,这是实现无限滚动的前提,在move()中将辅助图更换为真正的相应图片
    copyPic() {
    // 复制 图1节点 给first(第一张图)
    const first = this.picBox.firstElementChild.cloneNode(true)
    // 复制 最后一张图的节点 给last(最后一张图)
    const last = this.picBox.lastElementChild.cloneNode(true)
    // 在 ul尾部 增加 辅助图(第一张图)
    this.picBox.appendChild(first)
    // 在 ul首部元素前 增加 辅助图(最后一张图)
    this.picBox.insertBefore(last, this.picBox.firstElementChild) // 把参数1加在参数2前面
    // 增加后首先显示的是 辅助图(最后一张图) ,需要改成首先显示图1
    // 注意:图片数量增多,图片容器ul总宽度也要增加
    this.picBox.style.width = this.sliderWidth * this.picBox.children.length + "px"
    // !!千万别在css中设置,否则js里获取left值转数字会很烦,一直NAN!!
    this.picBox.style.left = -1 * this.sliderWidth + "px"
    }

    // 【点击左右按钮/点击圆点都会用到move()】
    // 触发轮播图的移动效果,如到辅助图则替换为真正的图,改变圆点样式,offset是位移
    move(offset) {
    // 移动ul改变当前显示图片
    // 注意:left是带单位的(字符串),parseFloat转为数字
    this.picBox.style.left = parseFloat(this.picBox.style.left) + offset + "px"

    // 如到辅助图则替换为真正的图【重点看】
    if (parseFloat(this.picBox.style.left) == 0) {
    // 去真正的尾图,即ul向左移动到图3的位置
    // sliders,3,轮播图数量;sliderWidth,轮播图框宽度
    this.picBox.style.left = -this.sliders * this.sliderWidth + 'px'
    } else if (parseFloat(this.picBox.style.left) == -(this.sliders + 1) * this.sliderWidth) {
    // 去真正的首图
    this.picBox.style.left = -this.sliderWidth + 'px'
    }

    // 给当前圆点加特殊样式,其他圆点抹掉样式,圆点数量num
    const num = this.indexBox.children[0].children.length
    for (let i = 0; i < num; i++) {
    this.indexBox.children[0].children[i].className = ""
    }
    // this.index,小圆点的索引,初始为1,在leftRight()中控制加减
    this.indexBox.children[0].children[this.index - 1].className = "active"
    }

    // 点击左右按钮可切换图片
    leftRight() {
    // 选中左侧按钮
    this.box.querySelector(".left-box").addEventListener("click", () => {
    console.log("left")

    // index为当前小圆点索引(初始图1为1),sliders为图片数量5
    // 如列表溢出则重置,连续点击出现空白图时为溢出
    if (this.index - 1 < 1) {
    // 当前位置左移1位却小于1时,index重置为5
    this.index = this.sliders
    } else { // 正常则当前图片索引index减1
    this.index--
    }

    // 调用move()移动轮播图,移动位移为 负 轮播图宽度
    this.move(-this.sliderWidth)
    })

    // 选中右侧按钮
    this.box.querySelector(".right-box").addEventListener("click", () => {
    console.log("right")

    // 同理,当前位置+1超出总轮播图数量时,重置index为1
    if (this.index + 1 > this.sliders) {
    this.index = 1
    } else {
    this.index++
    }

    this.move(this.sliderWidth)
    })
    }

    // 自动播放
    play() {
    // 每隔2秒点击一次“向右”按钮
    this.auto = setInterval(() => {
    this.box.querySelector(".right-box").click()
    }, 2000);

    // 鼠标进入 轮播图盒子 时,清除定时器
    this.box.addEventListener("mouseenter", () => {
    clearInterval(this.auto)
    })

    // 鼠标移出 轮播图盒子 时,继续执行定时器
    this.box.addEventListener("mouseleave", () => {
    this.auto = setInterval(() => {
    this.box.querySelector(".right-box").click()
    }, 2000);
    })
    }

    }

    // 传入的是Slider类构造函数中需要的轮播图父元素的id
    const slider = new Slider("#slider");
    // 那么绑定在Slider类构造函数中的属性、函数都会被引入
    </script>

点击列表,输出对应的索引

  • 易忘点:
    • document.createDocumentFragment()创建Fragment
    • document.createElement('a')创建新标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ol id="list"></ol>
<script type="text/javascript">
let list = document.getElementById("list");
let fragment = document.createDocumentFragment();
// 循环创建li,放入fragment,fragment放入ol
for (let i = 1; i <= 10; i++) { // 注意从1开始而不是0,毕竟最后输出索引是从1开始
let li = document.createElement("li");
li.innerHTML = "选项" + i;
// 绑定click事件
li.addEventListener("click", () => {
alert(i);
})
fragment.appendChild(li);
}
list.appendChild(fragment);
</script>

手写jsonp【没看】

  • jsonp是浏览器同源策略导致的跨域问题的一种解决策略
  • 原理:<script>请求资源时可绕过跨域限制,所以script中src地址可以是跨域地址服务器,可以任意动态拼接数据返回(符合js格式要求即可)。所以,<script>就可以获得跨域的数据,只要服务端愿意返回。具体例子可参考JS Web API AJAX
  • 注意:jsonp 只能发起GET请求需要服务的支持
  • 代码参考
,