基础知识-js篇

685 阅读28分钟

js

es6新增属性和方法

  • var let const
  • promise
  • 箭头函数
  • 解构赋值
  • 模板字符串
  • 数组新增方法:Array.from()、fill()、find() 和 findIndex()、includes()
  • 对象新增方法:Object.is()、Object.assign()、__proto__属性:Object.setPrototypeOf(), Object.getPrototypeOf()、Object.keys(), Object.values(), Object.entries()

变量类型

六种原始类型

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol

nullundefined 区别

null表示没有对象,即该处不应该有值

1) 作为函数的参数,表示该函数的参数不是对象

2) 作为对象原型链的终点

undefined表示缺少值,即此处应该有值,但没有定义

(1)变量被声明,但没有赋值时,就等于undefined。

(2)调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。 null和undefined转换成number数据类型

null 默认转成 0

undefined 默认转成 NaN

值类型和引用类型

  • 值类型有:Boolean, String, Number, Undefined, Null
  • 引用类型有:所有包含Object类的,比如Date,Array, Function等,
  • 引用类型的值指向同一个内存地址,两者引用同一个值,因此b修改属性时,a的值也随之改动
  • 参数传递上,值类型按值传递,引用类型是共享传递 原因:值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且 按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值,保证过大的对象等不会因为不停复制内容而造成内存的浪费。
function foo(a){    a = a * 10; }
function bar(b){
   b.value = 'new'; }
var a = 1;
var b = {value: 'old'}; foo(a);
bar(b);
console.log(a); // 1
console.log(b); // value: new

类型判断

typeof

  • 识别所有值类型
  • 识别函数
  • 判断是否是引用类型(不能再细分)
  • 如果需要准确判断引用类型可以用instancof,或者Object.prototype.toString.call()参考 使用
        if(typeof tmp  == 'number') {
            console.log(tmp)
        }

image.png image.png

typeof null 为什么等于 object?

原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判
断为 object 类型, null 的二进制全是0,所以执行 typeof 时会返回“ object ”。

这个bug是第一版Javascript留下来的。在这个版本,数值是以32字节存储的,由标志位(1~3个字节)和数值组成。标志位存储的是低位的数据。这里有五种标志位:
000:对象,数据是对象的应用。
1:整型,数据是31位带符号整数。
010:双精度类型,数据是双精度数字。
100:字符串,数据是字符串。
110:布尔类型,数据是布尔值。\

拷贝

参考

浅拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为null 和 undefined 不能转化为对象,所以第一个参数不能为null或 undefined,会报错。
  • Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

2.扩展运算符...

扩展运算符是一个 es6的新特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。语法:let cloneObj = { ...obj };

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

数组方法实现数组浅拷贝

Array.prototype.concat()

  • concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
  • 如果省略了所有参数,可以实现一个数组的浅拷贝。
let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

Array.prototype.slice()

  • slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。
  • 如果省略所有参数,就可以实现一个数组的浅拷贝。
let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

日常使用 JSON.parse(JSON.stringify(arr))

缺点

  • JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将js对象转成JSON字符串,再使用JSON.parse把字符串解析成js对象。
  • 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

手写深拷贝

      function deepClone(obj = {}) {
        if (typeof obj !== "object" || obj == null) {
          return obj;
        }
        // 初始化返回结果
        let result = obj instanceof Array ? [] : {};
        for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
            // 递归调用
            result[key] = deepClone(obj[key]);
          }
        }
        return result;
      }

类型计算

+号

参考

  • 两个操作数如果是number则直接相加出结果
  • 如果其中一个操作数为string,则将另一个操作数隐式的转换为string,然后进行字符串拼接得出结果
  • 如果操作数为对象或者是数组这种复杂的数据类型,那么就将两个操作数都转换为字符串,进行拼接
  • 如果操作数想boolean这种简单数据类型,那么就将操作数转换为number相加得出结果
  • [] + {} 因为[]会被强制转换为"",然后+运算符链接一个{},{}强制转换为字符串就是"[object Object]"
  • {}当作一个空代码块,+[]是强制将[]转换为number,转换的过程是+[] => +"" => 0 最终结果就是0
[] + {} //"[object Object]"
{} + [] // 0
{} + 0 //0
[] + 0 //"0"

字符串拼接

const a = 100 + 10     //110
const b = 100 + '10'  //'10010'
const c = true + '10'  //'true10'
//带有字符串的数字计算,parseInt解析字符串返回整数
const d = 100 + parseInt('10') //110
//还可以用Number()进行转换,而且这个还可以转换小数
const d = 100 + Number('10.5') //110.5

运算符==,===

==的情况

100 == '100' //true
0 = '' //true
0 == false //true
false == '' //true
null == undefined //true,
//除了 == null 之外,其他一律用 ===
const obj = { x : 100 }
if(obj.a == null) {}
//相当于 if(obj.a === null || obj.a === undefined){}

原型和原型链

基本结构

    class Student {
        constructor(name, number) {
            this.name = name
            this.number = number
        }
        sayHi() {
            console.log('hello World')
        }
    }
    
    //通过类 new 对象/实例
    const xialuo = new Student('夏洛',100)

继承

extends

//父类
class People {
    constructor(name) {
        this.name= name
    }
    eat() {
        console.log(`${this.name} eat something`)
    }
    
 //子类
 class Student extends People {
     constructor(name, number) {
         super(name)
         this.number = number
     }
     sayHi() {
         console.log("hello")
     }
 }
 
 //实例
 const xialuo = new Student('夏洛', 100)
 xialuo.sayHi()
 xialuo.eat()

简单实现jQuery

class jQuery {
  constructor(selector) {
    const result = document.querySelectorAll(selector)
    const length = result.length
    for (let i = 0; i < length; i++) {
      this[i] = result[i]
    }
    this.length = length
    this.selector = selector

  }
  get (index) {
    return this[index]
  }
  each (fn) {
    for (let i = 0; i < this.length; i++) {
      const elem = this[i]
      fn(elem)
    }
  }
  on (type, fn) {
    return this.each(elem => {
      elem.addEventListener(type, fn, false)
    })
  }
}

// 使用,导入
<script src="./js/jquery.js"></script>
//声明一个jQuery对象
const $ = new jQuery("p")
//使用其中的方法
$.each((elem) => console.log(elem.nodeNmae))

原型链

参考1 参考2

原型

  • 只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型对象)
  • 原型对象自动获得一个名为constructor的属性(指回与之关联的构造函数)

原型链

概念:JS的原型链是指原型与原型层层相连接的过程即为原型链。

  • 当我们访问一个对象的属性或方法时,如果这个对象内部不存在这个属性,那么它就会去通过__proto__往上找原型对象,
  • 原型对象中又有__proto__,就一直往下找下去,这就是原型链的概念,
  • 原型链的尽头是Object.prototype,然后Object.prototype.__proto__为null。

image.png

原型链api

isPrototypeOf()方法确定两个对象之间的关系

console.log(Person.prototype.isPrototypeOf(person)) //true

Object.getPrototypeOf()返回传入对象的原型对象,相当于person.__proto__

console.log(Object.getPrototypeOf(person) == Person.prototype) //true

Object.create() 创建一个新对象,同时为其指定原型

let biped = {
    numLegs = 2
}
let person = Object.create(biped);
person.name ='Matt'
console.log(person.name)
console.log(person.numLegs);
console.log(Object.getPrototypeOf(person) == biped) //true 这里说明创建的新对象的__proto__是biped.prototype

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上

function Person() {}
Person.prototype.name = 'Nichilas'
let person1 = new Person();
console.log(person1.hasOwnProperty("name");//false
person1.name = "Grag"
console.log(person1.hasOwnProperty("name");//true name来自实例

instanceof 类型判断

参考

object instanceof constructor
// 等同于
constructor.prototype.isPrototypeOf(object)

instanceof原理: 检测 constructor.prototype是否存在于参数 object的 原型链上。instanceof 查找的过程中会遍历object的原型链,直到找到 constructorprototype ,如果查找失败,则会返回false,告诉我们,object 并非是 constructor 的实例。 代码实现

function instanceof(L, R) { //L是表达式左边,R是表达式右边
    const O = R.prototype;
    L = L.__proto__;
    while(true) {
        if (L === null)
            return false;
        if (L === O) // 这里重点:当 L 严格等于 0 时,返回 true 
            return true;
        L = L.__proto__;
    }
}

题目

var A = function (){}
        A.prototype.n = 1
        var b = new A()
        A.prototype = {
            n:2,
            m: 3
        }
        var c = new A()
        console.log(A.prototype === b.__proto__) // false
        console.log(A.prototype === c.__proto__) // true
        console.log(b.__proto__ === c.__proto__) // false
        console.log(b.n, b.m, c.n, c.m) // 1 undefined 2 3
       /**
        * 因为原型对象变了
        *
       */

作用域

  • 块级用域
  • 函数作用域
  • 全局作用域

作用域链

当所需要的变量在所在的作用域中查找不到的时候,它会一层一层向上查找,直到找到全局作用域还没有找到的时候,就会放弃查找。这种一层一层的关系,就是作用域链。

自由变量

  • 一个变量在当前作用域没有定义,但被使用
  • 向上级作用域,一层一层依次寻找,直至找到为止
  • 如果到全局作用域都没找到,则报错xx is not defined image.png

let、const、var的区别

(1)作用域:,let和const具有块级作用域,var是函数级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升:var存在变量提升,let和const不存在变量提升,即变量只能在声明之后使用,如果在变量声明之前使用就会报错,这就是暂时性死区。

(3)给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,而let和const不会。

(4)重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(6)初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向:let和const都是ES6新增的用于创建变量的语法。 var和let创建的变量都可以重新赋值(更改指针指向)。而const声明的变量是不允许改变引用的地址。

区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️
能否改变指针指向✔️✔️×

const对象的属性可以修改吗

const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

var a = 0; console.log(window.a) // 0 
let b = 1; console.log(window.b) // undefined

闭包

定义

  • 闭包是指有权访问另一个函数作用域中变量的函数
  • 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方,当然只是先在定义的地方找,如果没找到就去执行地方找
  • 当然并不是说就不在执行的地方找,而是先在函数定义的地方找,如果没找到就返回去从函数执行的地方开始找。 应用场景
  • 函数作为参数
  • 函数作为返回值
  • 总的来说就是函数定义的地方和执行的地方不一样,就会产生闭包

image.png

实际应用
将数据隐藏,只对外提供方法

function createCache() {
    const data = {}
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}

const c = createCache()
c.set('a',100)
console.log(c.get('a'))
  • 题目
// 每次点击都会出现10,应为a.addEventListener不是立即执行函数,
// 当点击时,alert(i)自由变量i就会向上级作用域寻找,直到找到全局作用域i=10
    let i, a
    for (i = 0; i < 10; i++) {
        a = document.createElement('a')
        a.innerHTML = i + '<br>'
        a.addEventListener('click', function (e) {
            e.preventDefault()
            alert(i)
        })
        document.body.appendChild(a)
    }
// 每次点击都是对应的序号,因为此时i为块级作用域,每次循环都会生产独立的块级作用域
// 所以不会互相影响
    let  a
    for (let i = 0; i < 10; i++) {
        a = document.createElement('a')
        a.innerHTML = i + '<br>'
        a.addEventListener('click', function (e) {
            e.preventDefault()
            alert(i)
        })
        document.body.appendChild(a)
    }

闭包的变量会一直存在
因为闭包函数会一直引用闭包定义的地方的变量,所以就不会释放这个变量

this

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。this是在运行时进行绑定的,并不是编写时绑定,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式, 在实际开发中,this 的指向可以通过五种调用模式来判断。

  • 第一种是默认绑定(函数调用模式),当一个函数不是一个对象的属性 ,直接作为函数来调用时,this 指向全局对象。
  • 第二种是隐式绑定(方法调用模式),当一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是new绑定(构造器调用模式),当一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 显式绑定(apply 、 call 和 bind 调用模式),apply,call,bind这三个方法都可以显式的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传给被调用函数的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法传参和call一样,但是返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
  • 这四种方式,使用new绑定优先级最高,然后是显式绑定,隐式绑定,默认绑定。
  • 第五种箭头函数,ES6中的箭头函数并不会使用四条标准的绑定规则,而是继承外层第一个普通函数的this,如果外层没有普通函数则this指向window

箭头函数

参考 参考

概念:1)箭头函数没有 prototype (原型),所以箭头函数本身没有this。2)箭头函数不会创建自己的this而是继承外层第一个普通函数的this,如果外层没有普通函数则this指向window 注意

  1. 定义字面量方法,this的意外指向。
const obj = {
  array: [1, 2, 3],
  sum: () => {
    // 根据上文学到的:外层没有普通函数this会指向全局对象
    return this.array.push('全局对象下没有array,这里会报错'); // 找不到push方法
  }
};
obj.sum();

上述例子使用普通函数或者ES6中的方法简写的来定义方法,就没有问题了,因为普通函数符合隐式绑定的规则

// 这两种写法是等价的
sum() {
  return this.array.push('this指向obj');
}
sum: function() {
  return this.array.push('this指向obj');
}

this练习

参考题目 参考题目

call,apply,bind

参考
调用call/apply/bind的必须是函数, call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。

语法:

fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

参数

thisArg(可选):

  1. funthis指向thisArg对象
  2. 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean

param1,param2(可选): 传给fun的参数。

  1. 如果param不传或为 null/undefined,则表示不需要传入任何参数.
  2. apply第二个参数为数组,数组内的值为传给fun的参数。

返回值:

  • call/apply:fun执行的结果 ;
  • bind:返回fun的拷贝,并拥有指定的this值和初始参数

区别:

call与apply的唯一区别

传给fun的参数写法不同:

  • apply是第2个参数,这个参数是一个数组:传给fun参数都写在一个数组中,但传给fun的参数并不是数组,而是数组里的值。
  • call从第2~n的参数都是传给fun的。

call/apply与bind的区别

执行

  • call/apply改变了函数的this上下文后马上执行该函数
  • bind则是返回改变了上下文后的函数,不执行该函数 返回值:
  • call/apply 返回fun的执行结果
  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数。

手写call

arguments对象

arguments对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引0处。例如,如果一个函数传递了三个参数,你可以以如下方式引用他们:

arguments[0]
arguments[1]
arguments[2]
function func1(a, b, c) {
  console.log(arguments[0]);
  // expected output: 1

  console.log(arguments[1]);
  // expected output: 2

  console.log(arguments[2]);
  // expected output: 3
}

func1(1, 2, 3);

arguments对象不是一个 Array 。它类似于Array,但除了length属性和索引元素之外没有任何Array属性。例如,它没有 pop 方法。但是它可以被转换为一个真正的Array

javaScript中的Array.prototype.slice.call(arguments)能将有length属性的对象转换为数组(特别注意: 这个对象一定要有length属性)

var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);

// ES2015
const args = Array.from(arguments);
const args = [...arguments];
//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
Function.prototype.myCall = function (context, ...args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    const result = context[key](...args)
    //删除添加的属性
    delete context[key]
    //返回函数调用的返回值
    return result
}

手写apply

Function.prototype.myApply = function(context, args) {
    context = context || window
    args = args ? args : []
    const key = Symbol()
    context[key] = this
    const res = context[key](...args)
    delete context[key]
    return res
}

手写bind

bind方法执行结果返回的是一个未执行的方法,执行时可以继续传入参数,实现了函数的柯里化,提高参数的复用视频参考 掘金参考
基础版

    Function.prototype.bind2 = function (context) {
        const func = this
        let args = [...arguments].slice(1)
        const binded = function () {
            func.apply(context, args.concat([...arguments]))
        }
        return binded
    }
    
    //使用
    function addStuff(age, birth) {
        this.age = age;
        this.birth = birth;
    }
    const adddStuffToObj = addStuff.bind(obj, 26)
    addStuffToObj(1993);

进阶版
构造函数的情况,当函数使用new生成实例stuff时,stuff拥有自身的属性和方法,这时候如果是基础版的bind,那么在生成实例stuff时,由于context在第一次addStuff.bind(null,26),this绑定了null。所以在生成实例时,newAddStuff上的属性和方法都不会绑定到stuff上而是绑定到null上。所以需要对bind进行改造,判断调用bind的是实例还是普通函数,如果是实例则让this指向该实例。

问题:如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么? 不会,bind没有改变原函数,单纯返回一个绑定了目标对象的新函数

// 使用
const newAddStuff = addStuff.bind(null, 26)
const stuff = new newAddStuff(1993)
    Function.prototype.bind2 = function (context) {
        const func = this
        let args = [...arguments].slice(1)
        const binded = function () {
            if(this instanceof binded) {
                func.apply(this, args.concat([...arguments]))
            }else {
                func.apply(context, args.concat([...arguments]))
            }
        }
        return binded
    }

完整版

  • 在进阶版中,虽然实现了功能但是没有继承调用函数的原型链,所以令bined函数的原型链指向addStuff中的原型链,但是如果直接令bined.prototype = addStuff.prototype那么当bined.prototype修改时会影响到bined.prototype。
  • 解决方案,创建一个新的函数,令新函数的原型链指向addStuff的原型链,然后令bined.prototype等于新函数的实例,原型链关系如下图 IMG_20210813_081633.jpg
    //使用
    const adddStuffToObj = addStuff.bind(obj, 26)
    addStuffToObj(1993);
    Function.prototype.bind2 = function (context) {
        const func = this
        let args = [...arguments].slice(1)
        let emptyFunc = function(){}
        const binded = function () {
            if(this instanceof binded) {
                func.apply(this, args.concat([...arguments]))
            }else {
                func.apply(context, args.concat([...arguments]))
            }
        }
        emptyFunc.prototype = this.prototype
        binded.prototype = new emptyFunc()
        return binded
    }

防抖函数

场景:监听一个输入框,文字变化后触发change事件,如果直接用keyup事件,那么会频繁触发change事件,防抖的作用就是等用户输入结束或者暂停时,才会触发change事件。
定义:动作停止后的时间超过设定的时间时执行一次函数

function debounce(fn, delay = 500) {
     let timeout = null; // 创建一个标记用来存放定时器的返回值
     return function () {
       clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
       timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
         fn.apply(this, arguments);
         timeout = null
       }, delay);
     };
   }
   function sayHi() {
     console.log('防抖成功');
   }
 
   var inp = document.getElementById('inp');
   inp.addEventListener('input', debounce(sayHi));
 

节流

场景:拖拽一个元素时,要随时拿到该元素被拖拽的位置,直接用drag事件,则会频繁触发,很容易导致卡顿,节流的作用是无论拖拽速度多快,都会每隔100ms触发一次
定义:一定时间内触发的操作只执行一次,保持一个触发的频率,无论触发多快。

function throttle(fn, delay = 100) {
     let timeout = null; // 创建一个标记用来存放定时器的返回值
     return function () { // 如果还有定时任务就return不执行
       if (timeout) {
           return
       }
       timeout = setTimeout(() => { 
         fn.apply(this, arguments);
         timeout = null
       }, delay);
     };
   }
   function sayHi() {
     console.log('节流成功');
   }
 
   var inp = document.getElementById('div1');
   inp.addEventListener('input', throttle(sayHi));

异步

参考

JS为啥是单线程

js作为浏览器脚本语言,其主要用途是与用户互动,以及操作DOM。这就决定了它只能是单线程,否则会带来很复杂的同步问题。(假设JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?)

为什么需要异步

JS是单线程语言,一次只能同时做一件事情 ,遇到等待(网络请求,定时任务)时浏览器不能卡住,所以需要异步,解决单线程等待的问题。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

应用场景

  • 网络请求,如ajax图片加载
  • 定时任务,如setTimeout
  • 图片加载
//图片加载
console.log('start')
let img = document.creatElement('img')
img.onload = function () {
    console.log('loaded')
}
img.src = '/xxx.png'
console.log('end')
  • 定时器
//setTimeout
console.log(100)
setTimeout(function () {
    console.log(200)
}, 100)
console.log(300)
//setInterval
console.log(100)
setInterval(function () {
    console.log(200)
}, 1000)
console.log(300)

Promise

介绍

Promise是ES6的新特性,它是一个用来传递异步消息的对象。它有两个特点:

  • 对象状态不受外界影响,Promise有三个状态:pending(进行中)、resolved(已完成)、rejected(已失败)。它的状态仅由异步操作结果决定。
  • 一旦状态改变,就不会再改变。Promise有两种状态改变:pending到resolved,pending到rejected,只要这两种情况发生,状态就不会再改变。

使用

//定义一个promise
function getData(url) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: url,
            success(data) {
                //成功时通过resolve将结果返回出去,触发then
                resolve(data)
            },
            error(err) {
                //失败时将通过reject将结果返回出去,触发catch
                reject(err)
            }
        })
    }
}
//使用
getData(url1).then(data1 => {
    console.log(data1)
    return getData(url2)
}).then(data2 => {
    console.log(url3)
    return getData(url3)
}).then(data3 => {
    console.log(data3)
}).catch(err => console.error(err))
}

加载图片

function loadImg(src) {
    return new Promise(
        (resolve, reject) => {
            const img = document.createElement('img')
            img.onload = () => {
                reslove(img)
            }
            img.onerror = () => {
                const err = new Error(`图片加载失败 ${src}`)
                reject(err)
            }
            img.src = src
        }
    )
}
const url1 = 'https: //img.mukewang'
const url2 = 'https://img.mukewang2'
    loadImg(url1).then(img1 => {
    console.log(img.width)
    //return img1会传入下一个then的参数中,
    //如果return 是 promise实例则会等实例执行完调用then并将返回值传入then
    return img1
}).then(img1 => {
    console.log(img1.height)
    return loadImg(url2)
}).then(img2 => {
    console.log(img2.width)
    return img2
}).then(img2 => {
    console.log(img2.height)
}).catch(ex => console.error(ex))
promise.all
function promiseAll(arr) {
  return new Promise((resolve, reject) => {
    if(!Array.isArray(arr)) {
      throw new TypeError('argument is not a array')
    }
    let resArr = []
    let count = 0
    arr.forEach((item, index) => {
      item.then(res => {
        count++
        resArr[index] = res
        if(count === arr.length) return resolve(resArr)
      }).catch(err => {
        return reject(err)
      })
    })
  })
} 

// test
let p1 = Promise.resolve(1)
let p2 = Promise.reject(2)

promiseAll([p1, p2]).then(res => {
  console.log(res) // [3, 1, 2]
}).catch(err => {
  console.log('错误'+err)
})

//错误2

promise状态

  • pending状态,不会触发then和catch
  • resolved状态,会触发后续的then回调函数
  • rejected状态,会触发后续的catch回调函数
  • 变化不可逆

改变状态

  • then正常返回resolved状态,里面有报错则返回rejected状态
  • catch正常返回resolved状态,里面有报错则返回rejected状态

练习题很重要

输出1,3;因为一开始promise.resolve()的状态为resolve所以触发then(),又因为then没有报错所以状态还是resolve,触发下一个then() image.png
输出1,2,3
image.png
输出1,2
image.png

async/await

async/await是消灭异步回调的终极武器,但和promise并不互斥

async/await 和 promise的关系

  • 执行async函数,返回promise对象包括状态
  • await相当于promise的then
  • try...catch 可捕获异常,代替promise的catch image.png

try...catch

!(async function () {
    const p4 = Promise.reject('err1') //rejected 状态
    try {
        const res = await p4
        console.log(res)
    } catch (ex) {
        console.error(ex)//try...catch 相当于promise catch
    }
})()

async/await调用顺序

主要一点就是异步的代码放在任务队列里,并不是马上执行需要等同步代码执行完在执行,像await async2() 后面的代码就相当于.then后面的内容,属于异步需要放到任务队列里,同步代码执行完在调用event loop,但是async函数是立即调用的 image.png

event loop

参考 event loop 是异步回调的实现原理

宏任务macroTask和微任务microTask

image.png
任务类型

  • 宏任务:整体代码script, setTimeout, setInterval, I/O
  • 微任务:Promise async/await, MutationObserver(监听DOM变化), process.nextTick(nodejs 类似定时任务)

微任务意义:

减少更新时的渲染次数 因为根据HTML标准,会在宏任务执行结束之后,执行微任务,并在下一次宏任务开始前DOM结构会重新渲染。如果在微任务中就完成数据更新,当宏任务结束时就可以得到最新的DOM结构。如果新建一个宏任务来做数据更新的话,那么渲染会执行两次

event loop 机制

//示例
console.log('Hi')
setTimeout(function cb1() {
    console.log('cb1')
},5000)
console.log('Bye') //Hi Bye cb1

事件循环 简洁版事件循环机制:

  1. 同步任务在主线程上执行,形成执行栈。
  2. 异步任务有了运行结果之后,就在任务队列之中放置一个事件。
  3. 当所有同步任务执行完毕,则读取任务队列里面的事件。先读取任务队列里面的全部微任务,再去读取宏任务,且每读取完一个宏任务,就检查有没有微任务,有的话把微任务都执行了。
  4. 不断重复

js如何执行

  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先执行同步任务再执行异步任务

微任务和宏任务的区别

  • 微任务:当前宏任务结束后,DOM渲染之前触发执行微任务
  • 宏任务:DOM渲染后触发下一个宏任务,如setTimeout 题目:注意alert可以打断js向下执行
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container').append($p1).append($p2).apped($p3) //同步任务
// 微任务:DOM渲染前触发
Promise.resolve().then(() => {
    console.log('length1',$('#container').children().length) // 3
    alert('Promise then') //DOM渲染了吗? -NO
})
// 宏任务:DOM渲染后触发
setTimeout(() => {
    console.log('length2',$('#container').children().length) // 3
    alert('setTimeout') //DOM渲染了吗 - yes
})

完整版总结

  1. 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”和“异步任务”;
  2. 同步任务进入主线程依次执行,形成执行栈;
  3. 异步任务会分为宏任务和微任务;
  4. 宏任务进入到Event Table(消息队列)中,并在里面注册回调函数,每当指定事件完成时,Event Table(消息队列)会将这个函数移到Event Queue(任务队列)中;
  5. 微任务也会进入到另一个Event Table(消息队列)中,并在里面注册回调函数,每当指定事件完成时,Event Table(消息队列)会将这个函数移到Event Queue(任务队列)中;
  6. 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue(任务队列),如果有任务,就全部执行,并渲染DOM,如果没有就执行下一个宏任务;
  7. 上述过程 不断重复,这就是Event Loop事件循环;

题目考察

image.png

// 标出执行顺序
async function async1 () {
    console.log('async1 start') //3
    await async2()//4
    console.log('async1 end')//微任务 9
}

async function async2 () {
    console.log('async2')//5
}

console.log('script start') //1

setTimeout(function () {
    console.log('setTimeout')//11
}, 0)

async1() //2

new Promise(function (resolve) {
    console.log('promise1')//6
    resolve()//7
}).then (function () {
    console.log('promise2')//微任务 10
})
console.log('script end')//8

事件

DOM事件模型 事件流

存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

image.png

Dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。

事件流:

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

事件绑定

有3种为设置事件处理函数的方式:
传统注册事件

  1. HTML上:在HTML元素标签中使用onclick="add()",当元素被点击的时候就会触发add事件
  2. 事件源.onclik = function(){}:把处理函数赋给节点的对象的on<event>属性。 方法监听注册事件
  3. addEventListener:使用node.addEventListener(event, handler, capture)removeEventListener(event, handler)capture是一个布尔值,表示是否在捕获阶段响应
<div id="div1" onclick="add()">div1</div>
<div id="div2">div2</div>
<div id="div3">div3</div>
<script>
// 在html中使用onclick事件
    function add() {
        alert(111)
    }
// 事件源.onclick = function(){}
    let div2 = document.getElementById('div2');
    div2.onclick = function() {
        alert('事件被触发了')
    }
    div2.onclick = function() {
      alert('事件被触发了2') //事件被触发了2
  }
// addEventListener
    let div3 = document.getElementById('div3')
    div3.addEventListener('click', function() {
        alert('add事件被触发') //add事件被触发
    })
    div3.addEventListener('click', function () {
      alert('add事件1被触发') //add事件1被触发
    })
</script>

addEventListener

参考 addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。addEventListener有三个参数:

const btn = document.getElementById('btn1')
btn.addEventListener('click', event => {
    console.log('clicked')
})
 element.addEventListener(event, function, useCapture)复制代码
参数描述
event必须。字符串,指定事件名。 注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。 提示: 所有 HTML DOM 事件,可以查看我们完整的 HTML DOM Event 对象参考手册
function必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。
useCapture可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值: true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)false- false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)
区别

传统的注册方式
注册事件的唯一性:同一个元素同一个事件只能设置一个处理函数,最后注册的处理函数将会覆盖前面注册的处理函数 addEventListener注册方式
w3c标准推荐的方式,同一个元素同一个事件可以注册多个监听器(处理函数)并按注册顺序执行, IE9之前的IE不支持此方法,可使用attachEvent()代替

常用api
cosnt btn1 = document.getElementById('btn1')
btn1.addEventListenter('click', event => {
    event.preventDefault()
})
//常用api
event.preventDefault() //阻止默认行为
event.target //获取触发点击的元素
event.stopPropagation() //阻止冒泡

事件委托(代理)

参考 由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation) 优势:

  • 代码简洁
  • 减少浏览器内存占用
//html部分
<div id="div3">
    <a href="#">a1</a><br>
    <a href="#">a2</a><br>
    <a href="#">a3</a><br>
    <a href="#">a4</a><br>
    <button>加载更多...</button>
<div>

const div3 = document.getElementById('div3')
div3.addEventListenter('click', event => {
    event.preventDefault() //阻止默认行为
    const target = event.target
    if(target.nodeName === 'A') { //判断是否是a标签
        alert(target.innerHTML)
    }
})

通用代理函数

function bindEvent(elem, type, selector, fn) {
    if (fn == null) {
        fn = selector
        selector = null
    }
    elem.addEventListener(type, event => {
        const target = event.target
        if(selector) {
            if(target.matches(selector)) {
                fn.call(target, event)
            }
       } else {
          fn.call(target, event)
        }
    })
}

DOM操作

参考 参考

获取查找DOM元素

Ele.getElementById(idName)

通过id查找元素。返回的是元素DOM,如果页面上有多个相同ID的元素,则只会返回第一个元素,不会返回多个(原则上ID只有一个)

注意:没有element.getElementById(id)

Ele.getElementsByClassName(className)

通过class查找。返回的是类数组结构,要想进行forEach遍历,需要先转化为数组结构

const doms = document.getElementsByClassName('xxx')

const domsArr = Array.from(doms)

domsArr.forEach(dom=>{})

Ele.getElementsByTagName(tagName)

更具标签名获取元素,使用方式和getElementsByClassName一样。

Ele.querySelector(selectors) | Ele.querySelectorAll(selectors)

这两个是唯一支持使用选择器来查找元素的api,有个这个api我们在进行深层次查找的时候方便很多

  • document/element.querySelectorAll(CSSSelector):返回满足选择器的一组节点列表。
  • document/element.querySelector(CSSSelector): 返回第一个满足选择器的元素。

<div class="warp">
    <p>name<p>
    <p>age<p>
<div>

<p>...</p>


<script>
// 目标 获取到warp下面的p元素

1. 不使用querySelectorAll
cons warp = document.getElementsByClassName("warp")[0];
const allp =  warp.getElementsByTagName(p)

2. 使用querySelectorAll

const allp = document.querySelectorAll(".warp p")
 
</script>

querySelector 获取单个元素,querySelectorAll 获取多个元素返回类数组结构

给DOM增加样式

给元素增加样式

Ele.style.width = xxx

给元素增加class

Ele.className='aaa'  // 设置元素的class为aaa ,如果元素上原本有class则会覆盖

Ele.classList.add("aaa") // 给Ele新增aaa

Ele.className += " aaa"  // 给Ele新增aaa

Ele.classList.remove("aaa") //给Ele删除aaa

判断元素上是否有某个属性

Ele.classList.contains("aaa")  // 如果Ele上面的class类名是aaa返回true,否则返回false

操作DOM上的属性

新增属性

Ele.setAttribute("data-id", 1);

获取属性的值

Ele.getAttribute("data-id");

删除属性

Ele.removeAttribute("data-id");

面向dom元素的增删改查

创建DOM元素

 const p = document.createElement("p");

删除DOM元素

 Ele.remove(); // 删除ELe
 
 Ele.removeChild(clildEle) // 删除ELe中的子元素 childEle ->为dom节点
//因为如果按顺序删除,当把索引为0的节点删除后,原来为1的节点变为0;此时变量i已经变成1。所以需要从后往前删
var f = document.getElementById(``"f"``);
var childs = f.childNodes;
for(var i = childs.length - 1; i >= 0; i--) {
  f.removeChild(childs[i]);
}

复制DOM

Ele.cloneNode( true | false )

const box = document.getElementsByClassName("box")[0];
const p = document.createElement("p");
p.innerText = "欢迎关注码不停息微信公众号";
const p2 = p.cloneNode(true); // 复制一个p  参数true标识深度复制,如果p里面有子节点也复制过来
box.appendChild(p);
box.appendChild(p2);

如图,有得到了两个p标签,并都显示到了页面上去

image.png

插入DOM

Ele.appendChild(ele) 在Ele的最后插入ele

Ele.insertBefore(newele,ele) // 在Ele元素中的 ele元素前插入 newele

替换DOM

parentEle.replaceChild(newEle,oldEle)

  <body>
    <div class="box">
      <h1>微信公众号</h1>
    </div>

    <button id="btn">变换</button>
    <script>
      const btnDom = document.getElementById("btn");
      const box = document.getElementsByClassName("box")[0];
      const h1 = document.getElementsByTagName("h1")[0];
      const h2 = document.createElement("h2");
      h2.innerText = "码不停息";
      btnDom.onclick = function () {
        box.replaceChild(h2, h1);
      };

    </script>
  </body>

遍历DOM

使用方法Ele.parentNode

  • parentNode

查找指定节点的父节点

  • nextSibling

查找指定节点的下一个节点

  • previousSibling

查找指定节点的上一个节点

  • firstChild

查找指定节点的第一个字节点

  • lastChild

查找指定节点的最后一个字节点

  • childElementCount

返回子元素的个数,不包括文本节点和注释

  • firstElementChild

返回第一个子元素

  • lastElementChild

返回最后一个子元素

  • previousElementSibling

返回前一个相邻兄弟元素

  • nextElementSibling

返回后一个相邻兄弟元素

值得注意的是节点和元素并不相等

<body>
    <div id="box">
      <p>文件</p>
      <p>文件</p>
    </div>

    <script>
      const box = document.getElementById("box");
      console.log(box.firstChild); // 打印 text节点(换行)
      console.log(box.firstElementChild); // 打印p标签
    </script>
 </body>

获取设置DOM元素内容

参考

  • innerHTML:获取HTML文本结构内容
  • textContent:获取指定节点的文本及其后代节点中文本内容,也包括<script>和<style>元素中的内容,display:none的节点文本,而innerText不会。
  • innerText:获取指定节点的文本及其后代节点中文本内容,但不能获取<script>和<style>元素中的内容。由于 innerText 受 CSS 样式的影响,它会触发重排(reflow),但 textContent 不会。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <style>
        button{
            border:1px solid red;
        }
    </style>
    <div class="contain">
        北京上海广州<span>深圳厦门</span>陕西西安
        <p>台湾香港澳门</p>
    </div>
    <button onclick="myFunction()">我是按钮</button>
    <script>
        function myFunction(){
            console.log(event.type);
        }
        let container = document.querySelector("body");
        console.log("textContent的内容是:",container.textContent);
        console.log("innerText的内容是:",container.innerText);
        console.log("innerHTML的内容是:",container.innerHTML);
    </script>
</body>
</html>

判断元素节点类型

nodeType,一共有12种类型,见W3C

  <body>
    <div id="box">
      <p>文件</p>
      <p>文件</p>
    </div>

    <script>
      const box = document.getElementById("box");
      console.log(box.firstChild.nodeType); // 3 文本
      console.log(box.firstElementChild.nodeType); // 1 元素
    </script>
  </body>

总结

DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?

(1)创建新节点

  createDocumentFragment()    //创建一个DOM片段
  createElement()   //创建一个具体的元素
  createTextNode()   //创建一个文本节点

(2)添加、移除、替换、插入

appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)

(3)查找

getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();

(4)属性操作

getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);

获取浏览器宽高大满贯

获取实际屏幕宽高

image.png

const W  =  window.screen.width 
const H  =  window.screen.height

获取浏览器宽高

image.png

const W = window.outerWidth;
const H = window.outerHeight;

获取当前窗口宽高(浏览器视口宽高)

image.png

const W = window.innerWidth;
const H = window.innerHeight;

获取元素布局宽高

内容区域+内边距+边框 image.png

const W = element.offsetWidth;
const H = element.offsetHeight;

获取元素内容宽高

image.png

const W = element.scrollWidth;
const H = element.scrollHeight;

监听元素的尺寸变化

ResizeObserver

位置

元素位置

获取滚动后被隐藏页面的宽高

window.scrollX|window.scrollY 常用 \

image.png 和scrollTop一样只不过它只能用window. , 而且比较常用

const W = window.scrollX
const H = window.scrollY
const W = window.pageXOffset
const H = window.pageYOffset

scrollTop|scrollLeft image.png

const H = document.documentElement.scrollTop;
const W = document.documentElement.scrollLeft
offsetTop|offsetLeft获取元素相对定位元素位置

参考
它返回当前元素相对于其offsetParent元素的顶部内边距外侧的距离。offsetParent元素为距离最近的一个具有定位的祖宗元素的位置,或者最近的 table,``td,``th,``body元素。 image.png

const top = Ele.offsetTop;
const left = Ele.offsetLeft
getBoundingClientRect()

返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回lefttoprightbottomxywidth, 和 height这几个以像素为单位的只读属性用于描述整个边框。除了width 和 height 以外的属性是相对于可视窗口的左上角来计算的。width/height的大小为content+padding+border
1.获取相对位置

let div = document.querySelector(".blue")
console.log(div.getBoundingClientRect().left)

2.获取绝对位置

let div = document.querySelector(".test")
console.log(window.pageYOffset + div.getBoundingClientRect().top)

鼠标位置

参考

MouseEvent 属性:
e.screenX / e.screenY(只读) : 鼠标相对于屏幕
e.pageX : e.pageY(只读) 鼠标相对于文档 e.clientX / e.clientY(只读) : 鼠标相对于可视窗口
e.x / e.y : 上面两个的别名存在兼容性问题保险起见用上面那个
e.offsetX / e.offsetY(只读) : 事件发生时鼠标相对于事件源的坐标\

浏览器原生事件盘点

鼠标事件

事件集合
  • 单击事件
Ele.onclick = function () {
    console.log("onclick");
 };
  • 双击事件
Ele.ondblclick = function () {
    console.log("ondblclick");
 };
  • 右击事件
Ele.oncontextmenu = function () {
    console.log("oncontextmenu");
 };
  • 鼠标按下事件
Ele.onmousedown = function () {
    console.log("onmousedown");
 };
  • 鼠标移动事件
Ele.onmousemove = function () {
    console.log("onmousemove");
 };
  • 鼠标抬起事件
Ele.onmouseup = function () {
    console.log("onmouseup");
 };
  • 鼠标进来事件
// 鼠标移动到自身时候会触发事件,同时移动到其子元素身上也会触发事件
Ele.onmouseover = function () {
    console.log("onmouseover");
 };
// 鼠标移动到自身是会触发事件,但是移动到其子元素身上不会触发事件
 Ele.onmouseenter = function () {
    console.log("onmouseenter");
 };
  • 鼠标离开事件
// 鼠标移动到自身时候会触发事件,同时移动到其子元素身上也会触发事件
Ele.onmouseout = function () {
    console.log("onmouseout");
 };
 // 鼠标移动到自身是会触发事件,但是移动到其子元素身上不会触发事件
 Ele.onmouseleave = function () {
    console.log("onmouseleave");
 };
基于鼠标事件完成拖拽

qw00d-d0jqo.gif

  const box = document.getElementById("box");
  let nowW, nowH, flag;
  box.onmousedown = function (e) {
    nowW = e.clientX;
    nowH = e.clientY;
    flag = true;
    document.onmousemove = function (e) {
      if (!flag) return false;
      const moveX = e.clientX - nowW;
      const moveY = e.clientY - nowH;
      const left = parseInt(box.style.left || 0);
      const top = parseInt(box.style.top || 0);
      box.style.left = left + moveX + "px";
      box.style.top = top + moveY + "px";
      nowW = e.clientX;
      nowH = e.clientY;
    };
    document.onmouseup = function () {
      flag = false;
    };
    document.onmouseleave = function () {
      flag = false;
    };
  };

复制代码
基于鼠标事件完成自定义右键

w6pys-l06lx.gif

<body>
    <div id="box"></div>
    <div id="option">
      <div class="item">复制</div>
      <div class="item">放大</div>
      <div class="item">搜索</div>
    </div>
    <script>
      const box = document.getElementById("box");
      const option = document.getElementById("option");
      box.oncontextmenu = function (e) {
        const x = e.clientX;
        const y = e.clientY;
        option.style.display = "block";
        option.style.top = y + "px";
        option.style.left = x + "px";
        return false;
      };
      option.onclick = function () {
        this.style.display = "none";
      };
    </script>
  </body>

键盘事件

事件集合

参考MDN

  • keydown:当用户按下键盘上的任意键时触发,而且如果按住按住不放的话,会重复触发此事件。
  • keyup:当用户释放键盘上的键时触发。
  • keypress:当用户按下键盘上的字符键时触发(就是说用户按了一个能在屏幕上输出字符的按键keypress事件才会触发),而且如果按住不放的,会重复触发此事件(按下Esc键也会触发这个事件)。已经过时不推荐使用

注意keyCode属性已经被弃用可以改用key属性,输出不再是按键对应数字而是按键对应的字符串

基于键盘事件完成使用方向键移动div

t2wmh-qbf80.gif

<style>
  #box {
    position: relative;
    width: 100px;
    height: 100px;
    background-color: red;
  }
</style>
<body>
    <div id="box">
      <div id="move">静止</div>
    </div>
    <script>
        const box = document.getElementById("box");
        const move = document.getElementById("move");
        let lefts = box.style.left || 0;
        let tops = box.style.top || 0;
        document.addEventListener("keydown", function (e) {
          const code = e.key;
          console.log(code)
          move.innerHTML = "开始移动";
          switch (code) {
            case 'ArrowUp':
              move.innerHTML = "上";
              tops -= 5;
              break;
            case 'ArrowDown':
              move.innerHTML = "下";
              tops += 5;
              break;
            case 'ArrowLeft':
              move.innerHTML = "左";
              lefts -= 5;
              break;
            case 'ArrowRight':
              move.innerHTML = "右";
              lefts += 5;
              break;
            default:
              break;
          }

          box.style.top = tops + "px";
          box.style.left = lefts + "px";
        });
        document.addEventListener("keyup", function () {
          move.innerHTML = "静止";
        });
    </script>
  </body>

存储

cookie

概念:本身用于浏览器和server通讯,被借用到本地存储,cookie默认关闭页面失效也可以设置失效时间
缺点:最大存储4kb,http请求时需要发送到服务端,增加请求数据量,通过document.cookie='...'修改值,

localStorage和sessionStorage

概念:HTML5专门为存储而设计,最大可存5M; api简单易用(setItem, getItem); 不会随着http请求被发送出去

//使用,设置,存储时只能存字符串的形式
localStorage.setItem('a', 100)
//获取
localStorage.getItem('a')

//使用,设置,存储时只能存字符串的形式
sessionStorage.setItem('a', 100)
//获取
sessionStorage.getItem('a')

区别

  • localStorage数据会永久存储,除非代码或者手动删除
  • sessionStorage数据只存在于当前会话,浏览器关闭则清空

cookie localStorage sessionStorage 区别

  • 容量
  • api易用性
  • 是否跟随http请求发送出去

return

通常使用

return需要在函数里面使用,而在函数里面使用时,无论在哪里都可以结束当前的函数并返回return后面的内容

返回执行函数和返回函数的区别

可以看到如果return的内容需要执行,那么会先执行在返回。obj.a的内容是函数所以返回函数,而obj.a()是调用该函数,所以返回调用函数后的值

      obj = {
        a: function () {
          return '调用'
        }
      };
      
      function text() {
        return obj.a
      }

      function text1() {
        return obj.a()
      }
      console.log(text()) // ƒ () {return '调用'}
      console.log(text1()) // 调用

解构赋值

参考

循环

for...of

参考
for…of是ES6新增的遍历方式,允许遍历一个含有iterator(迭代器)接口的数据结构(包括 ArrayMapSetStringarguments对象等 )并且返回各项的值,普通的对象用for..of遍历是会报错的。可以使用break,continue和return。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

   let arr=["nick","freddy","mike","james"];
    for (let item of arr){
        console.log(item)
    }
//暑促结果为nice freddy mike james
//遍历对象
   let person={name:"老王",age:23,city:"唐山"}
   for (let item of person){
        console.log(item)//会报错
    }

for...in

  • 在使用for in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。
  • 如果不想遍历原型方法和属性的话,可以使用hasOwnProperty()方法可以判断某属性是不是该对象的实例属性
  • 为什么原型中的默认属性如toString()等方法不会被遍历出来,因为它们是不可枚举的
var arr = [1,2,3]
Array.prototype.a = 123
    
for (let index in arr) {
  let res = arr[index]
  console.log(res)
}
//1 2 3 123

for(let index in arr) {
    if(arr.hasOwnProperty(index)){
        let res = arr[index]
  		console.log(res)
    }
}
// 1 2 3

for...in和for...of的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator(迭代器)象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结:for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历 ArrayMapSetStringarguments对象等

对象

可枚举性

参考

设置对象属性是否可以被枚举

通过Object.defineProperty里的enumerable来设置false为不可枚举

var o = {
	name : [1, 2, 3],
	age : 34
}
Object.defineProperty(o, "name", {
	enumerable : false, 		// 不能枚举,表示在o对象被枚举时,name属性不可见
});
for(var v in o) {
	alert(o[v]);	// return : 34,仅仅只是枚举了age属性,而name属性正好被屏蔽了
}
alert(o.propertyIsEnumerable("name"));		// return:false ,不能枚举

判断对象属性是否可以被枚举

每个对象都有propertyIsEnumerable()方法,这个方法可以判断出指定的属性是否可枚举。
用法:obj.propertyIsEnumerable(“属性名”);

Array.prototype.a = 1
console.log(Array.prototype.propertyIsEnumerable('toString')); //false
console.log(Array.prototype.propertyIsEnumerable('a')); //true

对象遍历

参考Javascript 中,对象遍历常用的方法有以下5种:

  1. for...in
  2. Object.keys()
  3. Object.getOwnPropertyNames()
  4. Object.getOwnPropertySymbols()
  5. Reflect.ownKeys()

对象es6增强写法

    const obj = {
        run() {
        
        },
        eat() {
        
        }
    }

继承

参考
继承方式

  • 原型链继承
  • 借用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6类继承extends

1、原型链继承

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。

function SuperType() {
  this.superName = 'father'
}

SuperType.prototype.getSuperName = function() {
  return this.superName
}

function SonType() {
  this.sonName = 'son'
}

SonType.prototype = new SuperType()

SonType.prototype.getSonName = function() {
  return this.sonName
}

let instance = new SonType()
console.log(instance.getSuperName()) //father
console.log(instance.getSonName()) //son

原型链方案存在的缺点:多个实例对引用类型的操作会被篡改。

function SuperType(){
  this.colors = ["red", "blue", "green"];
}
function SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

2、借用构造函数继承

使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
原理版

function  SuperType(){
    this.color=["red","green","blue"];
}
function  SubType(){
    //这里是改变this的指向使this指向SubType,然后执行SuperType方法会对this进行赋值
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

简洁版

function Father(name, age) {
  this.name = name
  this.age = age
}

function Son(name, age, score) {
  Father.call(this, name, age)
  this.score = score
}

let son = new Son('刘德华', 18, 100)
console.log(son.name) //刘德华

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

3、组合继承

组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。
原理版

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

简洁版

function SuperType(name) {
  this.name = name
  this.colors = ["red", "blue", "green"]
}

SuperType.prototype.sayName = function() {
  alert(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}

SubType.prototype = new SubType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
  alert(this.age)
}
let instance = new SubType("Nicholas", 29)
console.log(instance)

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

4、原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}

这种原型式的继承,必须要有一个对象(person)作为另一个对象的基础,然后再根据需求进行修改,于是把person传入到了object(),然后返回一个新对象,这个新对象将person作为原型。yetAnotherPerson 和anotherPerson 都共享了引用性属性friends

function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}

var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

使用Object.create() 参考

let person = {
  name: "Nicholas",
  friends:["Shelby", "Court", "Van"]
}

let anotherPerson = Object.create(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")

let yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")

console.log(person.friends) //['Shelby', 'Court', 'Van', 'Rob', 'Barbie']

缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数, object.create()可以传递参数

5、寄生式继承

核心:在原型式继承的基础上,增强对象,返回构造函数。createAnother函数的主要作用是为构造函数新增属性和方法,以增强函数

function createAnother(original) {
  let clone = Object.create(original)
  clone.sayHi = function() {
    console.log("hi")
  }
  return clone
}

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
}

let anotherPerson = createAnother(person)
anotherPerson.sayHi() //hi
console.log(anotherPerson.name) //Nicholas

缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数。

6、寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承
简版

function instance (son, father) {
  let prototype = Object.create(father.prototype)
  prototype.constructor = son
  son.prototype = prototype
}

function Father (name) {
  this.name = name
}

function Son (name) {
  Father.call(this, name)
}

instance(Son, Father)
function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

7、ES6类继承extends

extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显式指定构造方法,则会添加默认的 constructor方法,使用例子如下。

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    
    // Getter
    get area() {
        return this.calcArea()
    }
    
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);
    
    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

function _inherits(subType, superType) {
  
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

es6模块化

准备工作,首先需要在HTML代码中引入js文件,并且设置为module

<script src="info.js" type="module"></script>

导出

// 1. 导出方式一
export {
    flag, sum
}
// 2. 导出方式二
export var num1 = 1000
export var height = 1.88
// 3. 导出函数/类
export function mul (num1, num2) {
    return num1 + num2
}
// 4. export default 不指定名字,让导入者自己命名,这种类型只能有一个
const address = "北京市"
export default address

导入

// 1. 大部分
import { mul } from "./aaa.js"
// 2. export default导出,不带花括号默认导出export default
import add form "./aaa.js"
// 3. 当导入东西很多时,可以统一导入,aaa为自己命名
import * as aaa from "./aaa.js"