前端基础之JavaScript

浅拷贝和深拷贝

区别

PS:深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

  • 浅拷贝
    • 只克隆对象最外部的一层;
    • 只拷贝对象的引用,而不深层次的拷贝对象的值;
    • 新对象和原对象指向堆内存中的同一地址,任何一个修改都会使得所有对象的值修改,因为它们公用一条数据
  • 深拷贝
    • 重新创造一个一模一样的对象;
    • 新对象跟原对象不共享内存,修改新对象不会改到原对象

浅拷贝的实现

  1. Object.assign()
  • Object.assign() 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
  • 第一个参数是目标对象,其余都是源对象。
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
let obj = {
    a: 1,
    b: 2
}
let obj1 = Object.assign({}, obj);
obj1.a = 3;
console.log(obj.a) // 3

PS:当object只有一层的时候,就是深拷贝。

let obj = {
    username: 'kobe'
};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}
  1. for … in 循环
// 只复制第一层的浅拷贝
function shallowClone(obj1) {
   var obj2 = Array.isArray(obj1) ? [] : {};
   for (let i in obj1) {
   obj2[i] = obj1[i];
  }
   return obj2;
}
var obj1 = {
	a: 1,
	b: 2,
	c: {
		d: 3
	}
}
var obj2 = shallowClone(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 4
alert(obj2.c.d); // 4,obj1和obj2共享数据
  1. 直接 = 赋值
let a = [0,1,2,3,4],
    b = a;
console.log(a === b); // true
a[0]=1;
console.log(a, b);// [1, 1, 2, 3, 4] [1, 1, 2, 3, 4]
  1. Array.prototype.concat()Array.prototpye.slice()
let arr = [1, 3, {username: 'kobe'}];
let arr2=arr.concat();    
let arr3=arr.slice();    

arr2[2].username = 'wade';
console.log(arr);
arr3[1]=2;
console.log(arr, arr3);

slice浅拷贝
PS(slice):

  • 对新对象的改变只影响引用类型数据。
  • 对于字符串、数字及布尔值(不是 String、Number 或者 Boolean 对象)来说,slice 会拷贝这些值到新的数组里。
  • 在新的数组里修改这些字符串或数字或是布尔值,将不会影响原来的数组。

⭐⭐深拷贝的实现

  1. JSON.parse方法

const oldObj = {
  a: 1,
  b: [ 'e', 'f', 'g' ],
  c: { h: { i: 2 } }
};

const newObj = JSON.parse(JSON.stringify(oldObj));

console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

缺点

  • 无法实现对函数 、RegExp等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • 对象有循环引用,会报错
  1. 循环和递归
    对Array、Date、RegExp对象进行特殊处理。
const deepClone = (obj) => {
	// 储存循环引用的数组
	let parents = [],
		children = [];

	const _clone = (obj) => {
		if (obj === null) return null;
		if (typeof obj !== "object") return obj;

		let child, proto;
		let typeString = Object.prototype.toString.call(obj);
		switch (typeString) {
			// 对数组做特殊处理
			case "[object Array]": {
				child = new Array();
				break;
			}
			// 对Date做特殊处理
			case "[object Date]": {
				child = new Date(obj.getTime());
				break;
			}
			// 对正则对象做特殊处理
			case "[object RegExp]": {
				let flags = "";
				if (obj.global) flags += "g";
				if (obj.ignoreCase) flags += "i";
				if (obj.multiline) flags += "m";
				child = new RegExp(obj.source, flags);
				break;
			}
			// 处理对象原型
			default: {
				proto = Object.getPrototypeOf(obj);
				// 利用Object.create切断原型链
				child = Object.create(proto);
				break;
			}
		}

		// 处理循环引用
		const index = parents.indexOf(obj);
		if (index != -1) {
			// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
			return children[index];
		}
		parents.push(obj);
		children.push(child);

		for (let i in obj) {
			// 递归属性
			child[i] = _clone(obj[i]);
		}
		return child;
	};
	return _clone(obj);
};

缺点:

  • Buffer对象、Promise、Set、Map等暂未进行处理
  • 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间

⭐防抖-节流

本质上是优化高频率执行代码的一种手段

防抖 Debounce

  • 定义:在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时。
  • 应用场景:
    • 用户在输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询ajax请求,这样可以有效减少请求次数,节约请求资源;
    • window的resize、scroll事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次;
  • 实现
    • 每触发一次事件,清除当前的timer计时,重新开始计时。
    • 只有当高频事件停止,最后一次事件触发的超时调用才能在wait时间后执行
function debounce(func, wait) {
    let timeout;

    return function () {
    	// 获取函数的作用域和变量
        let context = this; 
        let args = arguments; 
        
		// 每次事件被触发,都会清除当前的timeer,然后重写设置超时调用
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args);
        }, wait);
    }
}

节流 Throttle

  • 定义:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
  • 应用场景:
    • 鼠标连续不断地触发某事件(如点击),只在单位时间内只触发一次;
    • 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据;
    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断;
  • 实现:
  1. 时间戳
function throttled1(fn, delay = 500) {
    let oldtime = Date.now();
    return function (...args) {
        let newtime = Date.now();
        if (newtime - oldtime >= delay) {
            fn.apply(null, args);
            oldtime = Date.now();
        }
    }
}
  1. 定时器
function throttled2(fn, delay = 500) {
    let timer = null;
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args);
                timer = null;
            }, delay);
        }
    }
}

垃圾回收机制

JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。

  1. 标记清除
    第一阶段:标记。从 根结点出发遍历对象,对访问过的对象打上标记,表示该对象可达 。
    第二阶段:清除。对那些没有标记的对象进行回收,这样使得不能利用的空间能够重新被利用。
  2. 引用计数(少用)
    引用计数思路是对每个值都记录它被引用的次数,这种方式常常会引起内存泄漏(循环引用),低版本的IE使用这种方式。
    机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。

DOM

节点属性

  1. nodeType 节点类型(只读)
  • 1:元素节点
  • 3:文本节点
  • 2:属性节点
  • 8:注释
  • 9:document 文档
  • 11:DocumentFragment 文档片段
  1. nodeName 节点名称(只读)
  • 1:元素节点返回标签名
  • 3:文本节点统一返回 “#text”
  • 2:属性节点返回属性名称
  • 8:注释节点返回“#comment”
  • 9:文档节点返回“#document”
if (someNode.nodeType == 1){
	value = someNode.nodeName; // 会显示元素的标签名
}
  1. nodeValue 当前节点的值。(可读写)
  • 1:元素节点返回null
  • 3:文本节点返回节点的内容
  • 2:属性节点返回属性值
  • 8:注释节点返回注释文本
  • 9:文档节点返回null
  1. attributes 节点的属性集合,返回NamedNodeMap对象,类似实时集合。
  • 元素的每个属性都表示为一个Attr 节点,并保存在这个NamedNodeMap 对象中。
  • NamedNodeMap 对象包含下列方法:
    • getNamedItem(name),返回nodeName 属性等于name 的节点;
    • removeNamedItem(name),删除nodeName 属性等于name 的节点;
    • setNamedItem(node),向列表中添加node 节点,以其nodeName 为索引;
    • item(pos),返回索引位置pos 处的节点。
      attributes

节点关系

  1. childNodes 子节点集合
  • 返回一个NodeList 的实例。
  • NodeList 是一个类数组对象,用于存储可以按位置存取的有序节点。
  • 用中括号或使用item()方法访问NodeList 中的元素:
let firstChild = someNode.childNodes[0];
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length;
  1. parentnode 父节点
  • 每个节点都有一个父节点
  • 最顶端的parentNode是#document
  1. firstChild 第一个子节点
  2. lastChild 最后一个子节点
  3. nextSibling 后一个兄弟节点
  4. previousSibling 前一个兄弟节点
if (someNode.nextSibling === null){
	alert("Last node in the parent's childNodes list.");
} else if (someNode.previousSibling === null){
	alert("First node in the parent's childNodes list.");
}
  1. hasChildNodes() 判断是否存在子节点方法,返回 true 则说明节点有一个或多个子节点。

操纵节点

  1. appendChild(要添加的节点) 用于在childNodes 列表末尾添加节点,返回新添加的节点。(如果添加的是已存在的子节点,则相当于将子节点位置移动,而不会同一个节点出现在两个位置)
let returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); // true
alert(someNode.lastChild == newNode); // true
  1. insertBefore(要插入的节点,参照节点) 在参照节点前插入新的节点
  • 要插入的节点会变成参照节点的前一个同胞节点,并被返回。
  • 如果参照节点是null,则insertBefore()与appendChild()效果相
// 作为最后一个子节点插入
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild); // true
// 作为新的第一个子节点插入
returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); // true
alert(newNode == someNode.firstChild); // true
// 插入最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.childNodes.length - 2]); // true
  1. replaceChild(要插入的节点,被替换的节点) 用新节点替换旧节点。返回要替换的节点并从文档树中完全移除,要插入的节点会取而代之。
// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);
  1. removeChild(要删除的节点) 移除节点,返回被移除的节点。

Element元素节点

  • nodeName 或 tagName:获取元素的标签名。
    • 元素标签名始终以全大写表示
    • 推荐将标签名转换为小写形式再比较:
<div id="myDiv"></div>
let div = document.getElementById("myDiv");
alert(div.tagName); // "DIV"
alert(div.tagName == div.nodeName); // true

if (div.tagName.toLowerCase() == "div"){ // 推荐,适用于所有文档
// 做点什么
}
  • HTML属性
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
// 读取
let div = document.getElementById("myDiv");
alert(div.id); // "myDiv"
alert(div.className); // "bd"
alert(div.title); // "Body text"
alert(div.lang); // "en"
alert(div.dir); // "ltr"
// 修改
div.id = "someOtherId";
div.className = "ft";
div.title = "Some other text";
div.lang = "fr";
div.dir ="rtl";
  • 取得属性 getAttribute() 主要用于取得自定义属性的值
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
let div = document.getElementById("myDiv");
alert(div.getAttribute("id")); // "myDiv"
alert(div.getAttribute("class")); // "bd"
alert(div.getAttribute("title")); // "Body text"
alert(div.getAttribute("lang")); // "en"
alert(div.getAttribute("dir")); // "ltr"
  • 设置属性 setAttribute() 适用于HTML 属性,也适用于自定义属性。会被规范为小写。
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
div.setAttribute("id", "someOtherId");
div.setAttribute("class", "ft");
div.setAttribute("title", "Some other text");
div.setAttribute("lang","fr");
div.setAttribute("dir", "rtl");
  • 删除属性 removeAttribute() 不单单是清除属性的值,而是会把整个属性完全从元素中去掉
  • 创建元素 document.createElement()
// 创建元素
let div = document.createElement("div");
// 设置属性
div.id = "myNewDiv";
div.className = "box";
// 添加到<body>文档树
document.body.appendChild(div);

Text文本节点

  • nodeValue 或 data:获取节点中包含的文本。
  • length 或 nodeValue.length data.length:获取文本节点中包含的字符数量
  • 向节点末尾添加文本:appendData(text)
  • 删除文本:deleteData(offset, count)
  • 插入文本:insertData(offset, text)
  • 替换文本:replaceData(offset, count, text)
  • 拆分文本节点:splitText(offset)
  • 提取子字符串:substringData(offset, count)
  • 创建文本节点:document.createTextNode() (
let element = document.createElement("div");
element.className = "message";
let textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
let anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
// 插入多个文本节点时,节点之间不会有空格)
  • 规范化文本节点,即合并相邻的文本节点:element.normalize()

Comment注释节点

正则

普通字符

  • [xyz]:字符集合。匹配所包含的任意一个字符。
  • [^xyz]:负值字符集合。匹配未包含的任意字符。
  • [a-z]:字符范围。匹配指定范围内的任意字符。
  • [^a-z]:负值字符范围。匹配任何不在指定范围内的任意字符。
  • \w:匹配字母、数字、下划线。等价于 [A-Za-z0-9_]

特殊字符

  • ( ):标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。
  • . :匹配除换行符(\n、\r)之外的任何单个字符。
  • x|y:指明两项之间的一个选择,匹配 x 或 y。

❗限定符:出现次数

指定正则表达式的一个给定组件必须要出现多少次才能满足匹配

  • 贪婪:
    • * :匹配前面的子表达式零次或多次。={0,}
    • +:匹配前面的子表达式一次或多次。={1,}
    • ?:非贪婪模式,前面的字符最多只可以出现一次(0次、或1次)。={0,1}
  • 非贪婪:
    • {n}:n 是一个非负整数。匹配确定的 n 次。
    • {n, }:n 是一个非负整数。至少匹配 n 次。
    • {n,m}:m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。
  • 通过在 *、+ 或 ? 限定符之后放置 ?,该表达式从"贪婪"表达式转换为"非贪婪"表达式或者最小匹配。

定位符

  • ^:匹配输入字符串的开始位置。
  • $:匹配输入字符串的结束位置。
  • \b:匹配一个单词边界,即字与空格间的位置。
  • \B:非单词边界匹配。

修饰符

  • /i:ignore,不区分大小写。将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
  • /g:global,全局匹配。查找所有的匹配项。
  • /m:multi line,多行匹配。使边界字符 ^$ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
  • /s:特殊字符圆点 . 中包含换行符 \n。默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 /s 修饰符之后, . 中包含换行符 \n

❗常用正则

  1. 邮箱
    匹配邮箱

  2. 合法链接

var str = "http://www.runoob.com:80/html/html-tutorial.html";
var patt1 = /(\w+):\/\/([^/:]+)(:\d*)?([^# ]*)/;
arr = str.match(patt1);
for (var i = 0; i < arr.length ; i++) {
    document.write(arr[i]);
    document.write("<br>");
}
  1. 手机号码

数据类型

  • 基本类型(值类型):Number、String、Boolean、undefined、null、Symbol
  • 引用类型:Function、Object、Array

判断数据类型

  1. typeof
  • 返回值:string-“xxx”
  • 可判断类型:number、string、boolean、undefined、object、function、symbol、bigint
console.log(typeof 42);
// expected output: "number"

console.log(typeof 'blubber');
// expected output: "string"

console.log(typeof true);
// expected output: "boolean"

console.log(typeof undeclaredVariable);
// expected output: "undefined"
  1. instanceof
  • 返回值:boolean
  • 原理:检测构造函数的 prototype 属性(constructor.prototype)是否出现在某个实例对象的原型链上
  • 原型链可改变,不是百分百准确
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);

console.log(auto instanceof Car);
// expected output: true

console.log(auto instanceof Object);
// expected output: true
  1. Object.prototype.toString.call()
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"

⭐⭐⭐ES6

变量声明

  • let 声明变量
  • const 声明常量:一旦赋值后,变量指向的那个内存地址所保存的数据不得修改

var、let和const区别

  1. 作用域不同:var 是全局作用域和函数作用域;而let 和 const 都是块级作用域,仅在整个大括号内可见。
  2. 变量提升:var 存在变量提升,未声明使用默认值 undefined;let、const未提前声明会提示 ReferenceError。
  3. 重复声明:var 允许重复声明;而let、const不允许在相同作用域重复声明。
  4. 全局对象属性:var 属于全局对象 window 的属性,可用 this 访问;而let、const声明的全局变量不属于。

解构赋值

  • 通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量。
  • 解构是一种打破数据结构,将其拆分为更小部分的过程。
  • 最简单理解等号左边多个值,等号右边也可以有多个值
  1. 数组解构
let [a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20
  1. 对象解构
let person = {
	name: 'Matt',
	age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
  1. 字符串解构
const [a, b, c, d, e] = 'hello';
console.log(a);  // "h"
console.log(b);  // "e"
console.log(c);  // "l"
console.log(d);  // "l"
console.log(e);  // "o"

箭头函数

  1. 更简短的语法 (param1, param2, …, paramN) => { return expression; }
  • 当箭头函数只有一个参数时,可以省略参数的圆括号 param=>{ return expression; }
  • 当箭头函数的函数体只有一个 return 语句时,可以省略 return 关键字和方法体的花括号param => expression
  1. ⭐没有单独的 this
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。
  • 所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| 正确地指向 p 实例
  }, 1000);
}

var p = new Person();
  1. call()、apply()、bind()无法改变箭头函数中this的指向
  • 由于箭头函数没有自己的this指针,通过 call()、apply()、bind() 方法调用一个函数时,只能传递参数,不能绑定this,他们的第一个参数会被忽略。
var adder = {
  base : 1,

  add : function(a) {
    var f = v => v + this.base;
    return f(a);
  },

  addThruCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base : 2
    };

    return f.call(b, a);
  }
};

console.log(adder.add(1));         // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2
  1. 箭头函数不绑定arguments,建议用rest参数…解决
  • 箭头函数没有自己的arguments对象。
  • 在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。
function foo(arg1,arg2) {
    var f = (...args) => args[1];
    return f(arg1,arg2);
}
foo(1,2);  //2
  1. 不能使用new操作符(作为构造函数使用)
    箭头函数不能用作构造器,和 new一起用会抛出错误。
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
  1. 箭头函数没有prototype属性
  • 箭头函数没有原型属性。
var Foo = () => {};
console.log(Foo.prototype); // undefined
  1. 不能简单返回对象字面量
  • 要圆括号把对象字面量包起来。
var func = () => { foo: 1 }; // Calling func() returns undefined!
func = () => ({ foo: 1 }); // √

var func = () => { foo: function() {} }; // SyntaxError: function statement requires a namevar 
var func = () => ({ foo: function() {} }); // √

  1. 箭头函数不能换行
  • 箭头函数在参数和箭头之间不能换行。
  • 但是,可以通过在 ‘=>’ 之后换行,或者用 ‘( )’、’{ }'来实现换行
var func = ()
           => 1; // SyntaxError: expected expression, got '=>'
var func = (
  a,
  b,
  c
) => 1;

// 不会有语法错误

遍历

  • 利用 for…of 遍历数组
  • 利用 for…in 遍历对象中的属性

数组

数组去重

  1. indexOf
function removeDuplicate(arr){
	let newArr = [];
	for(let i = 0; i < arr.length; i++){
		if((newArr.indexOf(arr[i])==-1)
			newArr.push(arr[i]);
	}
	return newArr;
}
  1. Map
function removeDuplicate(arr){
	let m = new Map();
	let newArr = [];
	for(let num of arr){
		if(!m.has(num)){
			m.set(num, true);
			newArr.push(num);
		}
	}
	return newArr;
}
  1. Set
function removeDuplicate(arr){
	let s = new Set(arr);
	return [...s];
	// return Array.from(s)
}
  1. Object(类Map)
function removeDuplicate(arr){
	let obj = {};
	let newArr = [];
	for(let num of arr){
		if(!obj[num]){
			obj[num]=1;
			newArr.push(num);
		}
	}
	return newArr;
}
  1. sort
function removeDuplicate(arr){
	let obj = {};
	let newArr = [];
	arr.sort();
	for(let i = 0; i < arr.length-1; i++){
		if(arr[i]!==arr[i+1]){
			newArr.push(num);
		}
	};
	return newArr;
}
  1. filter
function removeDuplicate(arr){
	arr.filter(function(item, index, arr){
		return arr.indexOf(item, 0) === index;
	return arr;
}

字符串

❗查找特定字符串

事件和事件流

事件

HTML事件指文档或浏览器窗口发生的一些特定的交互瞬间。可以使用监听器(或事件处理程序)来预定事件,以便事件发生时执行相应的代码。

  • HTML 网页完成加载 onload
  • HTML 输入字段被修改 onchange
  • HTML 按钮被点击 onclick
    JavaScript事件处理程序可用于处理、验证用户输入、用户动作和浏览器动作:
  • 每当页面加载时应该做的事情
  • 当页面被关闭时应该做的事情
  • 当用户点击按钮时应该被执行的动作
  • 当用户输入数据时应该被验证的内容
  • 等等

事件流

事件流描述的是从页面中接收事件的顺序。

  • IE提出的事件流是事件冒泡,即从下至上,从目标触发的元素逐级向上传播,直到window对象。
  • NetScape提出的事件流是事件捕获,即从document逐级向下传播到目标元素。由于IE低版本浏览器不支持,所以很少使用事件捕获。
  • ECMAScript规范后,DOM事件流包括下面几个阶段。
    • 事件捕获阶段
    • 处于目标阶段
    • 事件冒泡阶段
      事件流

事件处理机制

  1. HTML事件处理程序
    以HTML 属性的形式来指定 onclick=“相应处理”
// 1. 直接包含精确的动作指令
<input type="button" value="Click Me" onclick="console.log(&quot;Clicked&quot;)"/>
// 2. 调用在页面其他地方定义的脚本<script>或外部文件
<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>
  1. DOM0
    在JavaScript 中指定事件处理程序的传统方式是把一个函数赋值给(DOM 元素的)一个事件处理程序属性
// 先取得引用操作对象的引用
let btn = document.getElementById("myBtn");
// 给对象的事件处理程序属性,比如onclick赋值一个函数
// 此时this指向该元素
btn.onclick = function() {
console.log(this.id); // "myBtn"
};
  1. DOM2
    DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener()和removeEventListener()。

⭐⭐⭐事件循环机制 EventLoop

单线程

  • JavaScript 是单线程的。
  • JavaScript 的主要用途是与用户互动,以及操作 DOM 。为了避免出现多个线程操作冲突,JS引擎中负责解释和执行 JavaScript 代码的线程只有一个,称为主线程。
  • 除了主线程之外,还存在其他的线程。例如:处理 AJAX 请求的线程、处理 DOM 事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。

同步和异步

为了解决单线程带来的进程阻塞,JS 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。

  • 同步是指在主线程上排队执行任务。只有前一个任务执行完毕,才能继续执行下一个任务。当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。
  • 异步是指任务不进入主线程,而进入任务队列。只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务。

⭐宏任务和微任务

在异步模式下,创建异步任务主要分为宏任务微任务两种。

  • 宏任务是由宿主(浏览器、Node)发起的;
  • 而微任务由 JS 自身发起。
宏任务(Macrotask)微任务(Microtask)
script(整体代码块)Async/Await
setTimeoutPromise.[then/catch/finally]
setIntervalprocess.nextTick(Node环境)
UI 渲染queueMicrotask
I/OMutationObserver(浏览器环境)
postMessagerequestAnimationFrame(有争议)
setImmediate(Node环境)

EventLoop

  1. 判断宏任务队列是否为空
    • 不空 --> 执行最早进入队列的任务 --> 执行下一步
    • 空 --> 执行下一步
  2. 判断微任务队列是否为空
    • 不空 --> 执行最早进入队列的任务 --> 继续检查微任务队列空不空
    • 空 --> 执行下一步

因为首次执行宏队列中会有 script(整体代码块)任务,JS 解析完成后,在异步任务中,会先执行完所有的微任务。

总结:执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

Event对象

  • type:返回触发事件的类型,如"click"就代表点击事件,"mouseover"代表鼠标经过。
  • bubbles:获得当前触发事件的类型是否冒泡,true支持冒泡,false不支持。
  • eventPhase:事件传导至当前节点时处于什么状态。1:事件处于捕获状态,2:事件处于真正的触发者,3:事件处于冒泡状态。
  • target/srcElement:返回触发事件的元素,不一定是绑定事件的元素
  • currentTarget:返回事件的监听者,绑定事件的元素
  • button:声明了被按下的鼠标键,是一个整数。0:鼠标左键,1:鼠标中键,2:鼠标右键(古早IE0:没有按键,1:鼠标左键,2:鼠标右键,4:鼠标的中间键)如果按下了多个鼠标键,就把这些值加起来;
  • key:返回事件表示的键的标识符。如单个字母 (如 “a”, “W”, “4”, “+” 或 “$”),多个字母 (如 “F1”, “Enter”, “HOME” 或 “CAPS LOCK”)
  • preventDefault():阻止默认事件。
  • stopPropagation():阻止程序冒泡。

事件代理/代理delegate

  • 利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的事件。
  • 可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    var li_list = document.getElementsByTagName('li')
    // 普通绑定
    for(let index = 0;index<li_list.length;index++){
        li_list[index].addEventListener('click', function(ev){
            console.log(ev.currentTarget.innerHTML)
        })
    }
    // 事件委托
    var ul_dom = document.getElementsByTagName("ul");
	ul_dom[0].addEventListener("click", function (ev) {
		var event = ev || window.event;
		var target = event.target || event.srcElement;
		// 判断是否匹配目标元素
		if (target.nodeName.toLocaleLowerCase() === "li") {
			console.log("the content is: ", target.id);
		}
	});
</script>

⭐⭐自定义Event对象接口,继承该对象拥有on、off、emit三个方法

class MyEvent {
	constructor() {
		this._events = {};
	}
	// 监听event事件,事件触发时调用callback函数
	on(event, callback) {
		if (!this._events[event]) this._events[event] = [];
		this._events[event].push(callback);
		return this;
	}
	// 停止监听event事件
	off(event, callback) {
		if (this._events[event]) {
			if (!callback) {
				this._events[event] = [];
			} else {
				this._events[event].splice(
					this._events[event].indexOf(callback),
					1
				);
			}
		}
		return this;
	}
	//触发事件,并把参数传给事件的处理函数
	emit(event, ...args) {
		const callbacks = this._events[event];
		if (callbacks) {
			callbacks.forEach((callback) => {
				callback.apply(this, args);
			});
		}
	}
}
const emitter = new MyEvent();
const sayHi = (name) => console.log(`Hello ${name}`);
const sayHi2 = (name) => console.log(`Good night, ${name}`);

emitter.on("hi", sayHi);
emitter.on("hi", sayHi2);
emitter.emit("hi", "ScriptOJ");
// => Hello ScriptOJ
// => Good night, ScriptOJ

emitter.off("hi", sayHi);
emitter.emit("hi", "ScriptOJ");
// => Good night, ScriptOJ

⭐立即执行函数

  • 定义:此类函数没有声明,在一次执行后立即释放。适合做初始化工作。
  • 写法
    • (function () {} ()); W3C建议第一种
    • (function () {})();
    • function (){}(); 错误:只有表达式才能被执行符号执行
    • +/-/!functino test(){}(); 正确
    • var test = function(){}(); 正确:就成了立即执行函数,被执行符号执行的函数被忽略函数名

⭐⭐⭐闭包

  • 闭包函数:声明在一个函数中的函数,叫做闭包函数。
  • 闭包:一个内部函数在包含其所在的外部函数之外被调用,就会形成闭包。
  • 当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数内部的其它变量,如果返回的这个函数在外部被执行,就产生了闭包。
  • 通俗:两个函数嵌套把里面的函数保存到了外面,容易导致原有作用域链不释放,造成内存泄漏。
  • 特点:
    • 函数嵌套函数
    • 函数内部可以引用外部的参数和变量
    • 参数和变量不会被垃圾回收机制回收

example:输出0-9

function test() {
	var arr = [];
	for (var i = 0; i < 10; i++) {
   		arr[i] = function () {
       		document.write(i + " ");
     	}
 	}
	return arr;
} 
var myArr = test();
for (var j = 0; j < 10; j++) {
	myArr[j]();
}// 看起来是 0 1 2 3 4 5 6 7 8 9,其实是
// 10 10 10 10 10 10 10 10 10 10 ,最后执行同一个function里的i,此时i已变成10

因为myArr只被赋值了同一个函数体,i 是同一个,i 的值在最后调用循环才执行。(延迟执行)
在这里插入图片描述

使用立即函数修正

function test() {
	var arr = [];
	for (var i = 0; i < 10; i++) {
		// 用立即执行函数修正
		(function (j) {
			arr[j] = function () {
				document.write(j + " ");
			};
		})(i);
	}
	return arr;
} 
var myArr = test();
for (var j = 0; j < 10; j++) {
	myArr[j]();
}// 0 1 2 3 4 5 6 7 8 9 ,立即执行当前的i

使用立即执行函数相当于给myArr赋值了10个不同函数体,里面的值也不同。

闭包的特点

  1. 让外部访问函数内部变量成为可能
  2. 局部变量会常驻内存中
  3. 可以避免使用全局变量,防止全局变量污染
  4. 会造成内存泄露(有一块内存被长期占用,得不到释放)

闭包的作用

  1. 实现共有变量,不依赖于外部变量并能反复执行(e.g. 函数累加器)
// 1.累加器
function add() {
	var count = 0;
	function demo() {
		count++;
		document.write(count + " ");
	}
	return demo;
}// 1 2 3 4 5
var counter = add();
counter();
counter();
counter();
counter();
counter();
  1. 可以做缓存(存储结构)(e.g. eater)
// 2. 用作存储结构
function eater() {
	var food = "";
	var obj = {
		eat: function () {
			console.log("I am eating " + food);
			food = "";
		},
		push: function (myFood) {
			food = myFood;
		},
	};
	return obj;
}
var eater1 = eater();
eater1.push("banana");
eater1.eat();
// 缓存
function test() {
	var num = 100;
	function a() {
		num++;
		document.write(num + " ");
	}
	function b() {
		num--;
		document.write(num + " ");
	}
	return [a, b];
}// 101 100,调用的同一个num,根据顺序先执行了a的num++变成101,
// 在执行b的num--回到100
var myArr = test();
myArr[0]();
myArr[1]();
  1. 可以实现封装,属性私有化(e.g. Person())

  2. 模块化开发,防止污染全局变量

⭐⭐原型Prototype

Prototype

  • 原型是function对象的一个属性,定义了构造函数制造出的对象的公共祖先。
  • 通过该构造函数产生的对象,可以继承该原型的属性和方法
  • 原型也是对象。

查看方法

  • 对象如何查看原型–>隐式属性__proto__
  • 对象如何查看构造函数–>constructor

原型链

  • 原型链的增删改查
    • 增:后代不能增加父代属性,只能增加自己的
    • 删:后代不能删除父代属性,只能删除自己的
    • 查:从近的找,直到找到终端,终端都没有就是undefined
    • 改:后代不能修改父代属性,只能修改自己的
  • 对象自变量 var obj = {}(更简单)等同于 var obj = new Object();
  • 绝大多数对象的最后都会继承自Object.prototype
  • Object.create(原型)
    var obj = Object.create(null); // obj 无原型
    yxl1
    给obj加一个原型obj.__proto__ = {name:"sunny"}
    yxl2
    无法访问name属性
    yxl3
    ∴原型是隐式内部属性,人为加的原型系统不认

⭐call/apply/bind

  • 参数:第一个参数都是 this 的指向对象,其余参数是要传入的数据
  • 作用:
    • 改变this的指向
    • 借用别的对象的方法
    • 调用函数,因为apply,call方法会使函数立即执行 Object.prototype.toString.call(arr) // 判断类型
  • 区别:
    • 参数:call()/bind() 需要把实参按形参个数传进去,apply() 需要传一个数组
    • 返回值:bind() 返回调用的函数,call()/apply()是立即执行返回的是结果
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var perosn = new Person('deng', 100);
var obj = {}
Person.call(obj, 'chen', 300);
// 借用Person的方法把后两个参数传到obj里

1

function Person(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
function Student(name, age, sex, tel, grade) {
    Person.call(this, name, age, sex);
    this.tel = tel;
    this.grade = grade;
}
var student = new Student("sunny", 18, "female", 12345, 2016);

2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值