前端基础之JavaScript
浅拷贝和深拷贝
区别
PS:深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
- 浅拷贝
- 只克隆对象最外部的一层;
- 只拷贝对象的引用,而不深层次的拷贝对象的值;
- 新对象和原对象指向堆内存中的同一地址,任何一个修改都会使得所有对象的值修改,因为它们公用一条数据
- 深拷贝
- 重新创造一个一模一样的对象;
- 新对象跟原对象不共享内存,修改新对象不会改到原对象
浅拷贝的实现
- 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"}
- 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共享数据
- 直接 = 赋值
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]
- 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);
PS(slice):
- 对新对象的改变只影响引用类型数据。
- 对于字符串、数字及布尔值(不是 String、Number 或者 Boolean 对象)来说,slice 会拷贝这些值到新的数组里。
- 在新的数组里修改这些字符串或数字或是布尔值,将不会影响原来的数组。
⭐⭐深拷贝的实现
- 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
- 对象有循环引用,会报错
- 循环和递归
对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来判断;
- 实现:
- 时间戳
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();
}
}
}
- 定时器
function throttled2(fn, delay = 500) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
}
}
垃圾回收机制
JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
- 标记清除
第一阶段:标记。从 根结点出发遍历对象,对访问过的对象打上标记,表示该对象可达 。
第二阶段:清除。对那些没有标记的对象进行回收,这样使得不能利用的空间能够重新被利用。 - 引用计数(少用)
引用计数思路是对每个值都记录它被引用的次数,这种方式常常会引起内存泄漏(循环引用),低版本的IE使用这种方式。
机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收。
DOM
节点属性
- nodeType 节点类型(只读)
- 1:元素节点
- 3:文本节点
- 2:属性节点
- 8:注释
- 9:document 文档
- 11:DocumentFragment 文档片段
- nodeName 节点名称(只读)
- 1:元素节点返回标签名
- 3:文本节点统一返回 “#text”
- 2:属性节点返回属性名称
- 8:注释节点返回“#comment”
- 9:文档节点返回“#document”
if (someNode.nodeType == 1){
value = someNode.nodeName; // 会显示元素的标签名
}
- nodeValue 当前节点的值。(可读写)
- 1:元素节点返回null
- 3:文本节点返回节点的内容
- 2:属性节点返回属性值
- 8:注释节点返回注释文本
- 9:文档节点返回null
- attributes 节点的属性集合,返回NamedNodeMap对象,类似实时集合。
- 元素的每个属性都表示为一个Attr 节点,并保存在这个NamedNodeMap 对象中。
- NamedNodeMap 对象包含下列方法:
- getNamedItem(name),返回nodeName 属性等于name 的节点;
- removeNamedItem(name),删除nodeName 属性等于name 的节点;
- setNamedItem(node),向列表中添加node 节点,以其nodeName 为索引;
- item(pos),返回索引位置pos 处的节点。
节点关系
- childNodes 子节点集合
- 返回一个NodeList 的实例。
- NodeList 是一个类数组对象,用于存储可以按位置存取的有序节点。
- 用中括号或使用item()方法访问NodeList 中的元素:
let firstChild = someNode.childNodes[0];
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length;
- parentnode 父节点
- 每个节点都有一个父节点
- 最顶端的parentNode是#document
- firstChild 第一个子节点
- lastChild 最后一个子节点
- nextSibling 后一个兄弟节点
- 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.");
}
- hasChildNodes() 判断是否存在子节点方法,返回
true
则说明节点有一个或多个子节点。
操纵节点
- appendChild(要添加的节点) 用于在childNodes 列表末尾添加节点,返回新添加的节点。(如果添加的是已存在的子节点,则相当于将子节点位置移动,而不会同一个节点出现在两个位置)
let returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); // true
alert(someNode.lastChild == newNode); // true
- 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
- replaceChild(要插入的节点,被替换的节点) 用新节点替换旧节点。返回要替换的节点并从文档树中完全移除,要插入的节点会取而代之。
// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);
- 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
。
❗常用正则
-
邮箱
-
合法链接
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>");
}
- 手机号码
数据类型
- 基本类型(值类型):Number、String、Boolean、undefined、null、Symbol
- 引用类型:Function、Object、Array
判断数据类型
- 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"
- 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
- 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区别
- 作用域不同:var 是全局作用域和函数作用域;而let 和 const 都是块级作用域,仅在整个大括号内可见。
- 变量提升:var 存在变量提升,未声明使用默认值 undefined;let、const未提前声明会提示 ReferenceError。
- 重复声明:var 允许重复声明;而let、const不允许在相同作用域重复声明。
- 全局对象属性:var 属于全局对象 window 的属性,可用 this 访问;而let、const声明的全局变量不属于。
解构赋值
- 通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量。
- 解构是一种打破数据结构,将其拆分为更小部分的过程。
- 最简单理解等号左边多个值,等号右边也可以有多个值
- 数组解构
let [a, b] = [10, 20];
console.log(a); // 10
console.log(b); // 20
- 对象解构
let person = {
name: 'Matt',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
- 字符串解构
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"
箭头函数
- 更简短的语法
(param1, param2, …, paramN) => { return expression; }
- 当箭头函数只有一个参数时,可以省略参数的圆括号
param=>{ return expression; }
- 当箭头函数的函数体只有一个
return
语句时,可以省略return
关键字和方法体的花括号param => expression
- ⭐没有单独的 this
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。
- 所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // |this| 正确地指向 p 实例
}, 1000);
}
var p = new Person();
- 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
- 箭头函数不绑定arguments,建议用rest参数…解决
- 箭头函数没有自己的arguments对象。
- 在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。
function foo(arg1,arg2) {
var f = (...args) => args[1];
return f(arg1,arg2);
}
foo(1,2); //2
- 不能使用new操作符(作为构造函数使用)
箭头函数不能用作构造器,和 new一起用会抛出错误。
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor
- 箭头函数没有prototype属性
- 箭头函数没有原型属性。
var Foo = () => {};
console.log(Foo.prototype); // undefined
- 不能简单返回对象字面量
- 要圆括号把对象字面量包起来。
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() {} }); // √
- 箭头函数不能换行
- 箭头函数在参数和箭头之间不能换行。
- 但是,可以通过在 ‘=>’ 之后换行,或者用 ‘( )’、’{ }'来实现换行
var func = ()
=> 1; // SyntaxError: expected expression, got '=>'
var func = (
a,
b,
c
) => 1;
// 不会有语法错误
遍历
- 利用 for…of 遍历数组
- 利用 for…in 遍历对象中的属性
数组
数组去重
- 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;
}
- 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;
}
- Set
function removeDuplicate(arr){
let s = new Set(arr);
return [...s];
// return Array.from(s)
}
- 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;
}
- 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;
}
- 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事件流包括下面几个阶段。
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
事件处理机制
- HTML事件处理程序
以HTML 属性的形式来指定onclick=“相应处理”
。
// 1. 直接包含精确的动作指令
<input type="button" value="Click Me" onclick="console.log("Clicked")"/>
// 2. 调用在页面其他地方定义的脚本<script>或外部文件
<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>
- DOM0
在JavaScript 中指定事件处理程序的传统方式是把一个函数赋值给(DOM 元素的)一个事件处理程序属性
// 先取得引用操作对象的引用
let btn = document.getElementById("myBtn");
// 给对象的事件处理程序属性,比如onclick赋值一个函数
// 此时this指向该元素
btn.onclick = function() {
console.log(this.id); // "myBtn"
};
- 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 |
setTimeout | Promise.[then/catch/finally] |
setInterval | process.nextTick(Node环境) |
UI 渲染 | queueMicrotask |
I/O | MutationObserver(浏览器环境) |
postMessage | requestAnimationFrame(有争议) |
setImmediate(Node环境) |
- 判断宏任务队列是否为空
- 不空 --> 执行最早进入队列的任务 --> 执行下一步
- 空 --> 执行下一步
- 判断微任务队列是否为空
- 不空 --> 执行最早进入队列的任务 --> 继续检查微任务队列空不空
- 空 --> 执行下一步
因为首次执行宏队列中会有 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个不同函数体,里面的值也不同。
闭包的特点
- 让外部访问函数内部变量成为可能
- 局部变量会常驻内存中
- 可以避免使用全局变量,防止全局变量污染
- 会造成内存泄露(有一块内存被长期占用,得不到释放)
闭包的作用
- 实现共有变量,不依赖于外部变量并能反复执行(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();
- 可以做缓存(存储结构)(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]();
-
可以实现封装,属性私有化(e.g. Person())
-
模块化开发,防止污染全局变量
⭐⭐原型Prototype
Prototype
- 原型是function对象的一个属性,定义了构造函数制造出的对象的公共祖先。
- 通过该构造函数产生的对象,可以继承该原型的属性和方法
- 原型也是对象。
查看方法
- 对象如何查看原型–>隐式属性
__proto__
- 对象如何查看构造函数–>
constructor
原型链
- 原型链的增删改查
- 增:后代不能增加父代属性,只能增加自己的
- 删:后代不能删除父代属性,只能删除自己的
- 查:从近的找,直到找到终端,终端都没有就是undefined
- 改:后代不能修改父代属性,只能修改自己的
- 对象自变量
var obj = {}
(更简单)等同于var obj = new Object();
- 绝大多数对象的最后都会继承自Object.prototype
- Object.create(原型)
var obj = Object.create(null); // obj 无原型
给obj加一个原型obj.__proto__ = {name:"sunny"}
无法访问name属性
∴原型是隐式内部属性,人为加的原型系统不认
⭐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里
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);