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
null 和 undefined 区别
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)
}
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))
原型链
原型
- 只要创建一个函数,就会为这个函数创建一个prototype属性(指向原型对象)
- 原型对象自动获得一个名为constructor的属性(指回与之关联的构造函数)
原型链
概念:JS的原型链是指原型与原型层层相连接的过程即为原型链。
- 当我们访问一个对象的属性或方法时,如果这个对象内部不存在这个属性,那么它就会去通过__proto__往上找原型对象,
- 原型对象中又有__proto__,就一直往下找下去,这就是原型链的概念,
- 原型链的尽头是Object.prototype,然后Object.prototype.__proto__为null。
原型链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的原型链,直到找到 constructor 的 prototype ,如果查找失败,则会返回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
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声明的变量是不允许改变引用的地址。
| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | × | × |
| 是否添加全局属性 | ✔️ | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × |
const对象的属性可以修改吗
const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
var a = 0; console.log(window.a) // 0
let b = 1; console.log(window.b) // undefined
闭包
定义
- 闭包是指有权访问另一个函数作用域中变量的函数
- 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方,当然只是先在定义的地方找,如果没找到就去执行地方找
- 当然并不是说就不在执行的地方找,而是先在函数定义的地方找,如果没找到就返回去从函数执行的地方开始找。 应用场景
- 函数作为参数
- 函数作为返回值
- 总的来说就是函数定义的地方和执行的地方不一样,就会产生闭包
实际应用
将数据隐藏,只对外提供方法
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 注意
- 定义字面量方法,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(可选):
fun的this指向thisArg对象- 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 String、Number、Boolean
param1,param2(可选): 传给fun的参数。
- 如果param不传或为 null/undefined,则表示不需要传入任何参数.
- 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等于新函数的实例,原型链关系如下图
//使用
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()
输出1,2,3
输出1,2
async/await
async/await是消灭异步回调的终极武器,但和promise并不互斥
async/await 和 promise的关系
- 执行async函数,返回promise对象包括状态
- await相当于promise的then
- try...catch 可捕获异常,代替promise的catch
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函数是立即调用的
event loop
参考 event loop 是异步回调的实现原理
宏任务macroTask和微任务microTask
任务类型
- 宏任务:整体代码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
简洁版事件循环机制:
- 同步任务在主线程上执行,形成执行栈。
- 异步任务有了运行结果之后,就在任务队列之中放置一个事件。
- 当所有同步任务执行完毕,则读取任务队列里面的事件。先读取任务队列里面的全部微任务,再去读取宏任务,且每读取完一个宏任务,就检查有没有微任务,有的话把微任务都执行了。
- 不断重复
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
})
完整版总结
- 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”和“异步任务”;
- 同步任务进入主线程依次执行,形成执行栈;
- 异步任务会分为宏任务和微任务;
- 宏任务进入到Event Table(消息队列)中,并在里面注册回调函数,每当指定事件完成时,Event Table(消息队列)会将这个函数移到Event Queue(任务队列)中;
- 微任务也会进入到另一个Event Table(消息队列)中,并在里面注册回调函数,每当指定事件完成时,Event Table(消息队列)会将这个函数移到Event Queue(任务队列)中;
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue(任务队列),如果有任务,就全部执行,并渲染DOM,如果没有就执行下一个宏任务;
- 上述过程 不断重复,这就是Event Loop事件循环;
题目考察
// 标出执行顺序
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事件模型 事件流
存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
Dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
事件流:
(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
(2)目标阶段:真正的目标节点正在处理事件的阶段;
(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
事件绑定
有3种为设置事件处理函数的方式:
传统注册事件
- HTML上:在HTML元素标签中使用
onclick="add()",当元素被点击的时候就会触发add事件 - 事件源.onclik = function(){}:把处理函数赋给节点的对象的
on<event>属性。 方法监听注册事件 - 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标签,并都显示到了页面上去
插入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);
获取浏览器宽高大满贯
获取实际屏幕宽高
const W = window.screen.width
const H = window.screen.height
获取浏览器宽高
const W = window.outerWidth;
const H = window.outerHeight;
获取当前窗口宽高(浏览器视口宽高)
const W = window.innerWidth;
const H = window.innerHeight;
获取元素布局宽高
内容区域+内边距+边框
const W = element.offsetWidth;
const H = element.offsetHeight;
获取元素内容宽高
const W = element.scrollWidth;
const H = element.scrollHeight;
监听元素的尺寸变化
ResizeObserver
位置
元素位置
获取滚动后被隐藏页面的宽高
window.scrollX|window.scrollY 常用 \
和scrollTop一样只不过它只能用window. , 而且比较常用
const W = window.scrollX
const H = window.scrollY
const W = window.pageXOffset
const H = window.pageYOffset
scrollTop|scrollLeft
const H = document.documentElement.scrollTop;
const W = document.documentElement.scrollLeft
offsetTop|offsetLeft获取元素相对定位元素位置
参考
它返回当前元素相对于其offsetParent元素的顶部内边距外侧的距离。offsetParent元素为距离最近的一个具有定位的祖宗元素的位置,或者最近的 table,``td,``th,``body元素。
const top = Ele.offsetTop;
const left = Ele.offsetLeft
getBoundingClientRect()
返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回left, top, right, bottom, x, y, width, 和 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");
};
基于鼠标事件完成拖拽
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;
};
};
复制代码
基于鼠标事件完成自定义右键
<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>
键盘事件
事件集合
- keydown:当用户按下键盘上的任意键时触发,而且如果按住按住不放的话,会重复触发此事件。
- keyup:当用户释放键盘上的键时触发。
- keypress:当用户按下键盘上的字符键时触发(就是说用户按了一个能在屏幕上输出字符的按键keypress事件才会触发),而且如果按住不放的,会重复触发此事件(按下Esc键也会触发这个事件)。已经过时不推荐使用
注意keyCode属性已经被弃用可以改用key属性,输出不再是按键对应数字而是按键对应的字符串
基于键盘事件完成使用方向键移动div
<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(迭代器)接口的数据结构(包括 Array,Map,Set,String,arguments对象等 )并且返回各项的值,普通的对象用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 循环可以用来遍历 Array,Map,Set,String,arguments对象等
对象
可枚举性
设置对象属性是否可以被枚举
通过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种:
for...inObject.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()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"