这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
JavaScript
作用
-
增强页面动态效果 (如:下拉菜单、图片轮播、信息滚动等)
-
实现页面与用户之间的实时、动态交互(如:用户注册、登陆验证等)
类型
-
嵌入式
把 JS 代码写在标签
<script type="text/javascript"></script>之间。 -
外联式(外部式)
把 JS 代码写一个单独的外部文件中,文件以'
.js'为扩展名。<script src="文件名.js"></script>
js代码放置的位置
-
浏览器解释HTML时是按自上而下的顺序的
-
放在
<head>部分最常用的方式是在页面中head部分放置
<script></script>元素,浏览器解析head部分就会执行这个代码,然后才解析页面的其余部分。 -
放在
<body>部分JavaScript代码在网页读取到该语句的时候就会执行,所以前面的 script 就先被执行。
进行页面显示初始化的 JS 必须放在head里面,因为初始化都要求提前进行(设置 CSS 等)。
如果是通过事件调用执行的 function ,那么对位置没什么要求的。
基础知识
代码格式
语句;
注释
-
单行注释:
//注释内容 -
多行注释:
/*注释内容*/
变量
-
概念
从字面上看,变量是可变的量;从编程角度讲,变量是用于存储某种/某些数值的存储器。
-
声明变量
var 变量名; -
命名规则
变量必须使用字母、下划线
_或者美元符$开始。可以使用任意多个英文字母、数字、下划线
_或者美元符$组成。不能使用JavaScript关键词与JavaScript保留字。
注意:JS区分大小写
-
赋值
先声明再赋值
var 变量名; 变量名 = 值;var 变量名 = 值; -
数据类型
(1)基本数据类型
数字Number,字符串String,布尔Boolean,对空Null,未定义Undefined,Symbol
(2)引用数据类型
对象Object,数组Array,函数Function
-
检查数据类型
typeoftypeof 变量名:返回变量的数据类型typeof Null === 'Object' -
变量存储(栈内存 & 堆内存)
js变量都是保存在栈内存中的
基本数据类型(变量名+值)直接存储在栈内存中,值与值之间是独立的,修改一个变量不会影响其他变量
引用数据类型是保存在堆内存中的对象,每
new一个新对象,就会在堆内存中开辟一个新空间来存放对象,而变量保存的是对象的地址(引用),变量名及其对象地址保存在栈内存中。若两个变量保存的是同一个对象引用,则其中一个变量修改属性使,另一个变量也会收到影响。 -
深拷贝 & 浅拷贝
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝:复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
对象
-
对象的概念
JS中的所有事物都是对象。
Object是js的一种数据类型,称为对象,是一组数据和功能(函数)的集合,使用键值对来保存。
每个对象带有属性和方法。
-
对象的分类
内置对象:由ES标准中定义的对象,在任何ES的实现中都可以使用(如
Math、String)宿主对象:由JS运行环境提供的对象,目前主要指由浏览器提供的对象(如
BOM、DOM)自定义对象:开发者自己创建的对象
-
创建对象
(1)使用构造函数:
对象名 = new Object();(2)字面量法:
对象名 = { 属性1:值1, 属性2:值2, ...};(3)工厂模式:
// 工厂模式创建对象 function creatObject(a,b,...) { var obj = new Object(); obj.属性1 = a; obj.属性2 = b; ... obj.方法名 = function(){ } return obj; } 对象名 = creatObject(a,b,...); // 使用该方法创建的对象类别无法区分,可结合构造函数来解决该问题 -
对象的属性
反映该对象某些特定的性质的。如:字符串的长度、图像的长宽等。
添加/修改属性:对象.属性名 = 属性值;
删除属性:delete 对象.属性名;
- 对象的方法
能够在对象上执行的动作(函数)。如:表单的“提交”(Submit),时间的“获取”(getYear)等。
对象名 = {..., 方法名: function(){ 函数体 } , ...}
-
访问对象方法的语法
对象名.方法(); -
垃圾回收
将不需要再使用的对象赋值为
null,JS会自动进行回收。
函数
函数也是一个对象。函数中可以封装一些功能,在需要时进行调用。
-
创建函数
// 方法一:函数声明式 function 函数名(形参) { 函数体; return 返回值; //有返回值时 } // 方法二:匿名函数式 函数名 = function(){ 函数体 }; // 方法三:Function构造函数(不推荐) // 会导致解析两次代码(第一次是解析常规ECMAScript代码,第二次是解析传入构造函数中的字符串),性能低 函数名 = new Function( "形参1", "形参2", ... , "函数体" ); -
调用函数
函数名(实参); -
作用域链(变量作用的范围)
-
全局作用域
在函数外部声明的变量,称为全局变量
全局变量的作用域是全局的:网页的所有脚本和函数都能够访问
全局作用域在页面打开时创建,在页面关闭时销毁
在全局作用域中有一个全局对象
window,它代表浏览器的一个窗口在全局作用域中创建的变量会作为
window对象的属性,创建的函数会作为window对象的方法 -
局部作用域(函数作用域)
在函数内部声明的变量,称为局部变量
局部变量的作用域是局部的:只能在该函数内部访问
局部作用域在函数调用时创建,在函数执行完毕时销毁
每调用一个函数都会创建一个新的函数作用域,它们之间是独立的
在局部作用域中可以访问到全局变量(可以使用
window.变量名访问),在全局作用域中不能访问到局部变量当局部作用域访问一个变量时,先在自身作用域中寻找,若有就直接使用;否则向上一级作用域寻找,直到找到全局作用域
在局部作用域有变量/函数提升的特性
在函数中,不使用
var声明的变量会成为全局变量函数中的形参相当于在函数作用域中声明了变量
-
-
变量/函数预解析(变量/函数提升)
-
变量预解析(变量提升)
使用
var关键字声明的变量,会在所有代码执行前先被声明(不会提前赋值)。但如果不是var声明的变量,则不会被提前声明。 -
函数预解析(函数提升)
使用函数声明式创建的函数
function 函数名(){ },会在所有代码执行前被创建(不提前调用)。但使用匿名函数创建的函数不会被提前创建,则不能在声明前调用。
-
-
上下文对象 this
-
解析器在每次调用函数时,会向函数内部传递进两个隐含的参数:
this和arguments。this指向的是一个对象,称为函数执行的上下文对象。在调用函数时,传递的实参都会在
arguments中保存。 -
在绝大多数情况下,函数的调用方式决定了
this指向的对象以函数形式调用:
this指向window以对象的方法形式调用:
this指向调用方法的对象
-
-
构造函数(类)
-
构造函数与普通函数的区别:构造函数在调用时使用
new关键字 -
构造函数的执行流程:
(1)创建一个新的对象(实例对象)
(2)将实例对象设置为构造函数中的this
(3)逐行执行函数中的代码
(4)将实例对象作为函数的返回值返回
-
语法
// 创建 构造函数(类) function 构造函数名(形参) { 函数体 // 函数中的this指向调用该构造函数的实例对象 } // 调用 构造函数 实例对象名 = new 构造函数名(实参); -
使用
instanceof检查一个对象是否是一个类的实例返回值为布尔值
实例对象名 instanceof 类名(构造函数名)任何对象 instanceof Object === true
-
-
原型对象
prototype-
创建的每一个函数时,解析器都会向函数中添加一个属性
prototype,这个属性的值指向函数的原型对象。 -
如果函数作为普通函数调用
prototype没有任何作用;当函数以构造函数的形式调用时,它所创建的实例对象中都会有一个隐含的属性(使用__proto__访问该属性),指向该构造函数的原型对象。 -
原型对象就相当于一个公共的区域,所有同一个类的实例对象都可以访问到这个原型对象。因此,创建构造函数时,可以将对象中共有的属性和方法,统一设置到原型对象中,就可以使每个对象都具有这些属性和方法:
构造函数名.prototype.属性名/方法名 = ... -
原型对象也是对象,因此它也有原型对象。
-
当访问对象的一个属性或方法时,会先在对象自身中寻找,如果有则直接使用,如果没有则去原型对象中寻找,如果还没有就去原型的原型中寻找,直到找到Object对象的原型对象。
-
判断对象中是否有某个属性/方法
"属性/方法名" in 对象名:若对象或原型函数中有该属性/方法,则返回true对象名.hasOwnProperty("属性/方法名"):若对象自身中有该属性/方法,则返回true
-
-
闭包
-
闭包的条件
函数嵌套函数
内部函数使用外部函数的变量
调用外部函数
-
闭包到底是什么
通俗的理解:函数内部嵌套的函数
浏览器查看后理解:内部函数的作用域中,包含引用外部函数变量的一个对象
-
闭包的作用
延长了局部变量的生命周期
在外部控制局部变量
-
闭包的缺点
局部变量会长期驻留在内存中,可能会造成内存泄漏(IE9以下)
-
解决闭包带来的缺点
减少使用闭包;及时释放。
-
数组
-
概念
数组是一个值的集合,每个值都有一个索引号,从0开始,每个索引都有一个相应的值。
-
创建数组
var 数组名 = new Array(数组长度); 创建的新数组是空数组,没有值,输出时显示为
undefined。 虽然创建数组时,指定了长度,但实际上数组都是变长的,也就是说即使指定了长度,仍然可以将元素存储在规定长度以外。
-
数组赋值
数组名[数组长度] = 数据;var 数组名 = new Array(所有数据);var 数组名 = [所有数据]; -
添加元素:
数组名[下一个未用的索引] = 数据; -
使用数组元素:
数组名[索引] -
数组长度:
数组名.length -
arguments
在调用函数时,浏览器每次都会传递进两个隐含的参数:函数的上下文对象this、封装实参的对象arguments。
在调用函数时,传递的实参都会在arguments中保存。
arguments是一个类数组对象,可以通过索引来操作数据,也可以获取长度。
arguments有一个属性叫做
callee,这个属性指向当前正在执行的函数对象。function 函数名() { arguments.callee == 函数名; }
判断语句
-
if 语句
if(条件) { 条件成立时执行的代码 } else { 条件不成立时执行的代码 } -
三目运算:
条件?真语句:假语句 -
switch语句
switch(变量名){ case 值1: 条件成立时执行的代码 break; //退出switch循环 case 值2: 条件成立时执行的代码 break; case 值3: 条件成立时执行的代码 break; default: 条件均不成立时执行的代码 break; }
循环语句
-
while语句
while(条件表达式) { 循环体; } -
do while语句
do { 循环体; }while (条件表达式); -
for语句
for(条件表达式) { 循环体; } -
for...of...语句
for...of语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments对象等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。for (变量名 of 可迭代对象) { //在每次迭代中,将不同属性的值分配给变量。 //statements } -
for...in...语句(仅迭代自身的属性)for...in语句以任意顺序迭代一个对象的非Symbol类型的可枚举属性,包括继承的可枚举属性。常用于检查属性值。for (变量名 in 可迭代对象) { //在每次迭代中,将不同属性的值分配给变量。 //statements } -
跳出循环
- break语句:终止整个循环
- continue语句:阻止某次循环的执行
String字符串对象
-
定义字符串对象
var 字符串对象名 = '字符串内容'; -
访问字符串对象的属性length
字符串对象名.length -
访问字符串对象的方法
toUpperCase():将字符串小写字母转换为大写字符串对象名.toUpperCase()charAt():返回指定位置的字符。返回的字符是长度为1的字符串。字符串对象名.charAt(index)indexOf():返回指定的字符串首次出现的位置字符串对象名.indexOf(字符串值, 字符串中开始检索的位置)如果省略字符串中开始检索的位置参数,则默认从头开始检索。
split():将字符串分割为字符串数组,并返回此数组。字符串对象名.split(分割的位置,分割的次数)substring():提取字符串中介于两个指定下标之间的字符。字符串对象名.substring(开始位置,结束位置)如果省略结束位置的参数,则默认为结尾。
substr():提取指定数目的字符字符串对象名.substr(开始位置,提取字符串的长度)
Array数组对象
-
数组定义的方法
定义一个空数组:
var 数组名= new Array();定义时指定有n个空元素的数组:
var 数组名 =new Array(n);定义数组的时候,直接初始化数据:
var 数组名 = [元素1, 元素2, 元素3, ...]; -
数组元素使用
数组名[下标] = 值; -
length属性
用法:
数组对象名.length返回:数组的长度,即数组里有多少个元素
-
数组方法
-
增加
push():把元素依次添加到数组的末尾,并返回添加元素后的数组长度数组对象名.push(新元素1, 新元素2, ...)unshift():把元素依次添加到数组的前面,并返回添加元素后的数组长度数组对象名.unshift(新元素1, 新元素2, ...) -
删除
pop():删除数组最后一个元素,并返回删除的元素数组对象名.pop()shift():删除数组第一个元素,并返回删除的元素数组对象名.shift()splice():删除指定数量的元素、替换指定元素、在指定位置添加元素数组对象名.splice(开始删除位置的索引, 删除的数量, 添加的元素1, 添加的元素2, ...)delete 数组名[要删除的元素的索引]【注意】delete这种方式数组长度不变,此时删除的元素变为undefined了,但是也有好处原来数组的索引也保持不变,此时要遍历数组元素可以才用。
-
排序
reverse():颠倒数组元素顺序数组对象名.reverse()sort():使数组中的元素按照一定的顺序排列数组对象名.sort(函数名) -
拼接
concat():连接两个或多个数组数组对象名.concat(要连接的第一个数组, 第一个数组, ..., 第N个数组)join():指定分隔符连接数组元素数组对象名.join(分隔符)如果省略分隔符的参数,则默认使用逗号作为分隔符
-
选择
slice():选定数组中的元素数组对象名.slice(开始位置, 结束位置) -
filter测试是否满足条件
filter(callbackFn, thisArg)callbackFn:用来测试数组中每个元素的回调函数。返回true表示该元素通过测试,保留该元素;false则不保留。它接受以下三个参数:element:数组中当前正在处理的元素index:正在处理的元素在数组中的索引array:调用了filter()的数组本身thisArg:可选,执行callbackFn时用于this的值返回值:一个新的、由通过测试的元素组成的数组。如果没有任何数组元素通过测试,则返回空数组。
-
JavaScript编码原则
各司其责
-
HTML 负责内容、CSS 负责样式、JS 负责交互行为。
-
避免由 JS 直接操作样式,可以通过修改元素的 className 来更改样式。
-
纯展示类交互寻求零 JS 方案。
例如:实现点击按钮切换样式的功能,可以使用以下方式。
<body> <input id="modeCheckBox" type="checkbox "> <div class="content"> <header> <label id="modeBtn" for="modeCheckBox"></label> <h1>标题</h1> </header> <main> 正文... </main> </div> </body>.content { transition: background-color 1s, color 1s; } #modeCheckBox{ display: none; } #modeCheckBox:checked + .content { background-color: black; color: white; transition: all 1s; }
组件封装
组件是指Web页面上抽出来一个个包含模版(HTML)、样式(CSS)和功能(JS)的单元。在进行组件封装时,将HTML解耦进行模板化,将JavaScript解耦进行插件化,将通用的组件模型抽象出来形成组件框架,增加组件的复用性、灵活性和扩展性。
组件设计的原则:封装性、正确性、扩展性、复用性
实现组件的步骤:结构设计、展示效果、行为设计
三次重构:插件化、模板化、抽象化(组件框架)
轮播图案例
<div id="my-slider" class="slider-list"></div>
#my-slider{
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul{
list-style-type:none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected{
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected{
transition: opacity 1s;
opacity: 1;
}
.slide-list__control{
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous{
display: inline-block;
position: absolute;
top: 50%;
margin-top: -25px;
width: 30px;
height:50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0,0,0,0.2);
cursor: pointer;
opacity: 0;
transition: opacity .5s;
}
.slide-list__previous {
left: 0;
}
.slide-list__next {
right: 0;
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: '<';
}
.slide-list__next:after {
content: '>';
}
.slide-list__control-buttons,
.slide-list__control-buttons--selected{
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer;
}
.slide-list__control-buttons--selected {
background-color: red;
}
class Component{
constructor(id, opts = {name, data:[]}){
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render(opts.data);
}
registerPlugins(...plugins){
plugins.forEach(plugin => {
const pluginContainer = document.createElement('div');
pluginContainer.className = `.${name}__plugin`;
pluginContainer.innerHTML = plugin.render(this.options.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
render(data) {
/* abstract */
return ''
}
}
class Slider extends Component{
constructor(id, opts = {name: 'slider-list', data:[], cycle: 3000}){
super(id, opts);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(data){
const content = data.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
getSelectedItem(){
const selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
slideNext(){
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious(){
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
addEventListener(type, handler){
this.container.addEventListener(type, handler);
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
const pluginController = {
render(images){
return `
<div class="slide-list__control">
${images.map((image, i) => `
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span>
`).join('')}
</div>
`.trim();
},
action(slider){
let controller = slider.container.querySelector('.slide-list__control');
if(controller){
let buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
controller.addEventListener('mouseover', evt=>{
var idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', evt=>{
slider.start();
});
slider.addEventListener('slide', evt => {
const idx = evt.detail.index;
let selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
});
}
}
};
const pluginPrevious = {
render(){
return `<a class="slide-list__previous"></a>`;
},
action(slider){
let previous = slider.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
};
const pluginNext = {
render(){
return `<a class="slide-list__next"></a>`;
},
action(slider){
let previous = slider.container.querySelector('.slide-list__next');
if(previous){
previous.addEventListener('click', evt => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
};
const slider = new Slider('my-slider', {name: 'slide-list', data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'], cycle:3000});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
过程抽象
-
过程抽象:用来处理局部细节控制的一些方法、函数式编程思想的基础应用
-
纯函数:相同的输入会导致相同的输出、不产生副作用、不依赖外部状态,要尽量使用纯函数来提高代码的可维护性。
-
高阶函数HOF :以函数作为参数、以函数作为返回值、常用于作为函数装饰器,使用高阶函数可减少非纯函数的使用率。
function HOF(fn) { return function(...args) { return fn.apply(this, args); } }-
Once
为了能够让“只执行一次”的需求(一次性的异步交互)覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
function once(fn) { return function(...args) { if(fn) { const ret = fn.apply(this, args); fn = null; return ret; } } } -
Throttle
函数防抖节流:限制一个函数在一定时间内只能执行一次。
“非立即执行版” 函数防抖:触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
function throttle(fn, time = 500) { let timer; //刚开始没有设置timer,因此timer初始值为null return function(...args) { if(timer == null) { fn.apply(this, args); //调用函数fn timer = setTimeout(() => { timer = null; }, time); } } } -
Debounce
“立即执行版”函数防抖:触发事件后函数不会立即执行,而是在 n 秒后执行。
function debounce(fn, dur) { dur = dur || 100; var timer; return function() { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, arguments); }, dur); } } -
Consumer
延时调用函数。
function consumer(fn, time) { let tasks = [], timer; return function(...args) { tasks.push(fn.bind(this, ...args)); if(timer == null){ timer = setInterval(() => { tasks.shift().call(this); if(tasks.length <= 0) { clearInterval(timer); timer = null; } }, time); } } } -
Iterative
迭代操作可迭代数据中的多个元素。
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'; //判断数据是否可迭代 function iterative(fn) { return function(subject, ...rest) { if(isIterable(subject)) { const ret = []; for(let obj of subject) { //迭代 ret.push(fn.apply(this, [obj, ...rest])); } return ret; } return fn.apply(this, [subject, ...rest]); } }
-
代码优化
写代码最应该关注的是代码的使用场景。