1. JS 的对象
JS
对象中独有的特性:对象具有高度的动态性,因为JavaScript
赋予了使用在运行时为对象添加状态和行为的能力。JavaSCrpipt
允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。
使用内置函数 Object.getOwnPropertyDescripter
来查看设置的属性的描述属性。我们可以使用Object.defineProperty
来修改属性的特征。实际上JavaSCript
对象的运行时是一个"属性的集合",属性以字符串或者Symbol
为key
,以数据属性特征值或者访问器属性特征值为value
。
typeOf
:typeof
返回数据类型的字符串表达,unddefined
是声明了未赋值,如果直接使用没有定义的变量会直接提示is not defined
,值null
是一个字面量,它不像undefined
是全局对象的一个属性。null
是表示缺少的标识,指示变量未指向任何对象。把null
作为尚未创建的对象,也许更好理解。在APIs
中,null
常在返回类型是对象,但没关联值的地方使用。
// foo不存在,它从来没有被定义过或者是初始化过:
foo;
"ReferenceError: foo is not defined"
// foo现在已经是知存在的,但是它没有类型或者是值:
var foo = null;
foo;
null
null
与undefined
的不同点:
当检测null
或undefined
时,注意相等(==)
与全等(===)
两个操作符的区别 ,前者会执行类型转换:
typeof null // "object" (因为一些以前的原因而不是'null')
typeof undefined // "undefined"
null === undefined // false
null == undefined // true
null === null // true
null == null // true
!null //true
isNaN(1 + null) // false
isNaN(1 + undefined) // true
instanceof
是判断对象的具体类型,a instanceof b
是a
是否b
的一个实例,也就是说b
是不是a
的一个构造函数。
typeof
不能判断的是:null
与object
,与array
,instanceof
返回值为布尔类型。===
只能判断undefined
以及null
,- 什么时候赋值为
null
:1. 定义对象的时候,进行初始赋值,表名将要赋值为对象。编程严谨。2. 销毁对象。垃圾回收机制回收。 - 严格区别变量类型与数据类型:1. 数据的类型:分为基本类型以及对象类型。2. 变量的类型:基本类型(保存的基本类型的数据)以及引用类型(保存的地址)。
2. await 与 async
async
与await
可以不同时出现,但是如果函数里面是包含了await
那么这个函数必须要是async
,await
就是说他会等待这个promise
对象返回结果,一旦返回结果才会去执行下面的代码,在函数前面加上一个async
他会自动将这个函数转换为promise
对象,返回的结果就是resolve()
返回的内容。
async function getSyncData() {
let time = await getSyncTime()
let data = `endTime - startTime = ${time}`
return data
}
3. JS 数据
-
什么是数据:存储在内存中代表特定信息的东西。本是二进制数据。
-
什么是内存:内存条通电以后产生的可以存储数据的空间(临时的数据)。内存产生和死亡:内存条 == 》通电== 》存储数据== 》处理数据== 》断电== 》内存空间和数据都消失。
-
内存的两个数据:内存存储的数据以及地址值。内存分类:栈(全局变量、局部变量),堆(对象)
-
两个引用变量指向同一个对象:变量存储的是对象的内存地址。通过一个变量修改对象内部数据,其他变量看到的是修改之后的数据。两个引用变量指向同一个对象,让其中一个引用变量指向另一个对象,另一个引用变量依然指向前一个对象。要想改变一个引用对象,一个是改变地址,一个是改变对象的值。需要注意的是在函数中调用对象的时候,是将变量存储的对象的地址复制给了参数。注意这里的复制了一份地址。
-
改变一个引用变量的值,就是变量需要存储这个引用变量的地址值。
-
js
在调用函数时传递变量参数时,是值传递还是引用传递:理解1:都是(基本/地址值)传递。理解2:可能是值传递,也可能是引用传递(地址值)。 -
简单数据类型和复杂数据类型在内存中的存储
基本数据类型:
指的是 简单的数据类型,也叫值类型,有数字Number
、字符串String
、布尔Boolean
、未定义Undefined
、空Null
引用数据类型
指的是 复杂的数据类型, 也叫引用类型,有数组Array
、函数Function
、对象等
存可以分为栈区 和 堆区
栈区:用来存储用 基本类型的数据 和 引用类型数据的地址。
堆区:用来存储 引用类型数据的数据。
基本类型数据传递给函数的参数时:
基本类型数据在复制时(一个变量名赋值给另一个新的变量名),会在栈区申请一块的空间存放一个新的数据,并且新的变量名会指向新的数据。
引用类型数据传:递给函数的参数时
引用类型数据在复制时(一个变量名赋值给另一个新的变量名),会在栈区申请一块的空间存放堆区中的引用类型数据的地址,并且新的变量名会指向栈区中存放地址的空间。 -
什么时候必须使用['属性名']的方式:1. 属性名包含特殊字符(-或者空格),2. 变量名不确定(动态定义变量)。
-
test.call/apply(obj)
:临时让test
称为obj
的方法进行调用。 -
什么函数是回调函数:1. 你定义的 2. 你没有调用 3. 但最终他执行了(在某个时刻,或者在某个条件下)。
-
IIFE
:匿名函数自调用。作用:隐藏实现、不会污染外部(全局)命名空间。,用它来编写js
模块。
(function () {
console.log('---')
})()
this
的指向:是指向调用他的对象。如果使用call /apply
就是指向传入的对象比如:setColor.call(obj,'black')
,这里的setColor
函数中如果打印this
就是指向obj
。
比如var test = p.setColor test()
;这里的this
是指向的window
,因为在赋值给test
的时候,并没有执行那个函数。后面调用test
就是想当于window.test()
,所以这里的this
是指向的window
。this
是什么:所有函数内部都有一个变量this
,他的值是调用当前函数的当前对象。任何函数本质上都是通过某个对象来调用的。函数必须通过对象来调用。如果没有指定就是window
。- 如何确定
this
的值:test()
:window
,p.test()
:p
,new test()
:新创建的对象,p.call(obj)
:obj
- 关于分号的问题:
js
一条语句的后面可以不加分号。
在下面两种情况下不急分号会出问题:
- 小括号开头的前一句语句:匿名函数:在小括号前加也行,在第一条语句后面加也行。
var a = 'a'
;(function () {
console.log('a')
})()
- 中方括号开头的前一条语句:
var a = 4
;[1, 3].forEach((item) => {
console.log(item)
})
如果不加分号会理解为:4[1, 3].forEach()
。
-
函数的
prototype
:
1 . 函数的prototype
属性:
每个函数都有一个prototype
属性,他默认指向一个Object
空对象(即称为:原型对象),原型对象中有一个属性constructor
,他指向函数对象。
2 .给原型对象添加属性(一般都是方法):作用:函数的所有实例对象自动拥有原型中的属性(方法) -
显示原型与隐式原型:
1 每一个函数都有一个prototype
,也就是显示原型(属性)
2 每一个实例对象都有一个__proro__
可以称为隐式原型(属性)
3 对象的隐式原型的值为其对应构造函数的显示原型的值
4 内存结构
5 总结:
函数的prototype
属性:在定义函数时自动添加的,默认值为一个空Object
对象。
对象的__proto__
属性:创建对象时自动添加的,默认值为构造函数的prototype
属性值。
程序员能直接操作显示原型,但不能直接操作隐式原型(ES6之前)
上图是一个原型链的图解:
-
访问一个对象的属性时,
- 现在自身属性中查找,找到返回,如果没有,再沿着
__proto__
这条链向上查找,找到返回,日过最终没找到,返回undefined
. - 别名:隐式原型链。
- 作用:查找对象的属性(方法)。
- 现在自身属性中查找,找到返回,如果没有,再沿着
-
构造函数/原型/实体对象的关系---作用域链是找属性,变量,原型链找方法。
实例的__proto__
指向的是他的构造函数的原型对象(构造函数的显示原型(prototype
)) -
函数的显示原型指向的对象默认是空
Object
实例对象(但Ovject
不满足)。 -
所有函数都是
Function
的实例。包含他本身(Function
)。Function.__proto__ === FUnction.prototype
。 -
Object
的原型对象是原型链尽头:Object.prototype.__proto__=null
。 -
读取对象的属性值时:会自动到原型链中查找。
-
设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值。
-
方法一般定义在原型中,属性一般通过构造函数定义在对象本身上。
4. JS 知识
4.1 基本类型:
基本类型有6种:undefined
、nul
l、boolean
、string
、number
、symbol
;注意没有object
。
虽然typeof null
返回的是object
,但是null
不是对象,而是基本数据类型的一种。基本数据存储在栈内存,存储的值。复杂数据类型的值存储在堆内存中,地址(指向堆中的值)存储在栈内存。当我们吧对象赋值给另外一个变量的时候,赋值的是地址指向同一块内存空间,当其中一个对象改变时,另一个对象也会发生变化。
4.2 typeof 与 instanceof
首先 typeof
能够正确的判断基本数据类型,但是除了 null
, typeof null
输出的是对象。instanceof
是通过原型链判断的,A instanceof B,
在A
的原型链中层层查找,是否有原型等于B.prototype
,如果一直找到A的原型链的顶端(null;即Object.__proto__.__proto__
),仍然不等于B.prototype
,那么返回false
,否则返回true
。
function instance_of(L, R) {
//L 表示左表达式,R 表示右表达式
var O = R.prototype;
// 取 R 的显式原型
L = L.__proto__;
// 取 L 的隐式原型
while (true) {
if (L === null)
//已经找到顶层
return false;
if (O === L)
//当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
//继续向上一层原型链查找
}
}
4.3 for of,for in 和 forEach,map 的区别
for...of
循环:具有 iterator 接口,就可以用for...of
循环遍历它的成员(属性值)。for...of
循环可以使用的范围包括数组、Set
和 Map
结构、某些类似数组的对象、Generator
对象,以及字符串。for...of
循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。对于普通的对象,for...of
结构不能直接使用,会报错,必须部署了Iterator
接口后才能使用。可以中断循环。
for...in
循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
forEach
: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined
)。
map
: 只能遍历数组,不能中断,返回值是修改后的数组。
4.4 箭头函数
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定,回顾前面的例子,由于JavaScript
函数对this
绑定的错误处理,下面的例子无法得到预期结果:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
现在,箭头函数完全修复了this
的指向,this
总是指向词法作用域,也就是外层调用者obj
:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()
或者apply()
调用箭头函数时,无法对this
进行绑定,即传入的第一个参数被忽略.
4.5 词法作用域
要明白js中有预定义以及作用域链的特性。词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。Javascript
函数的作用域是词法作用域,比如下面的代码;我们说过,词法作用域是写代码的时候就静态确定下来的。Javascript
中的作用域就是词法作用域(事实上大部分语言都是基于词法作用域的)
var a = 2;
function foo() {
console.log(a); // 会输出2
}
function bar() {
var a = 3;
foo();
}
bar();
但是如果foo
函数是bar
的一个属性的话,会获取到3,这里结果是2;是因为在函数定义的时候已经确定了定义域。
词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。
4.6 call与apply、bind 函数
https://blog.csdn.net/u010176097/article/details/80348447
4.7 this的指向
4.8 Promise
主要用于异步计算:
- 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
- 可以在对象之间传递和操作
promise
,帮助我们处理队列
异步回调的问题:
- 之前处理异步是通过纯粹的回调函数的形式进行处理
- 很容易进入到回调地狱中,剥夺了函数
return
的能力 - 问题可以解决,但是难以读懂,维护困难
- 稍有不慎就会踏入回调地狱 - 嵌套层次深,不好维护
Promise
promise
是一个对象,对象和函数的区别就是对象可以保存状态,函数不可以(闭包除外)- 并未剥夺函数
return
的能力,因此无需层层传递callback
,进行回调获取数据 - 代码风格,容易理解,便于维护
- 多个异步等待合并便于解决
new Promise(
function (resolve, reject) {
// 一段耗时的异步操作
resolve('成功') // 数据处理完成
// reject('失败') // 数据处理出错
}
).then(
(res) => {console.log(res)}, // 成功
(err) => {console.log(err)} // 失败
)
Promise可以并行处理若干个异步任务:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});
有些时候,多个异步任务是为了容错。比如,同时向两个URL
读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()
实现:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
下面的代码是promise
实现一个发送Ajax
请求:
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
4.9 async/await
在async/await
之前,我们有三种方式写异步代码:嵌套回调、以Promise
为主的链式回调、使用Generators
。
但是,这三种写起来都不够优雅,ES7做了优化改进,async/await
应运而生,async/await
相比较Promise
对象then
函数的嵌套,与 Generator
执行的繁琐(需要借助co
才能自动执行,否则得手动调用next()
), Async/Await
可以让你轻松写出同步风格的代码同时又拥有异步机制,更加简洁,逻辑更加清晰。
async/await
特点
async/await
更加语义化,async
是“异步”的简写,async function
用于申明一个function
是异步的;await
,可以认为是async wait
的简写, 用于等待一个异步方法执行完成;
async/await
是一个用同步思维解决异步问题的方案(等结果出来之后,代码才会继续往下执行)
可以通过多层 async function
的同步写法代替传统的callback
嵌套
async function
语法
自动将常规函数转换成Promise
,返回值也是一个Promise
对象
只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
异步函数内部可以使用await
。
async function name([param[, param[, ... param]]]) { statements }
name: 函数名称。
param: 要传递给函数的参数的名称
statements: 函数体语句。
返回值: 返回的Promise对象会以async function的返回值进行解析,或者以该函数抛出的异常进行回绝。
async function dd(){console.log('ddd')}
dd().then(err => {console.log(err)})
await
语法
await
放置在Promise
调用之前,await 强制后面的代码等待,直到Promise
对象resolve
,得到resolve
的值作为await
表达式的运算结果
await
只能在async
函数内部使用,用在普通函数里就会报错。
错误处理
在async
函数里,无论是Promise reject
的数据还是逻辑报错,都会被默默吞掉,所以最好把await
放入try{}catch{}
中,catch
能够捕捉到Promise
对象rejected
的数据或者抛出的异常:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {reject('error')}, ms); //reject模拟出错,返回error
});
}
async function asyncPrint(ms) {
try {
console.log('start');
await timeout(ms); //这里返回了错误
console.log('end'); //所以这句代码不会被执行了
} catch(err) {
console.log(err); //这里捕捉到错误error
}
}
asyncPrint(1000);
async
函数返回一个Promise
对象。async
函数内部return
语句返回的值,会成为then
方法回调函数的参数,如下面代码:
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。async
函数内部抛出错误,会导致返回的 Promise
对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到:
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
async
函数返回的 Promise
对象,必须等到内部所有await
命令后面的Promise
对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
注意,上面代码中,await语句前面没有return
,但是reject
方法的参数依然传入了catch
方法的回调函数。这里如果在await
前面加上return
,效果是一样的。任何一个await
语句后面的 Promise
对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。如果有多个await
命令,可以统一放在try...catch
结构里面.
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一种方法是await
后面的 Promise
对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发:
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
await
命令只能用在async
函数之中,如果用在普通函数,就会报错:
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代码会报错,因为await
用在普通函数之中了。但是,如果将forEach
方法的参数改成async
函数,也有问题:
function dbFuc(db) { //这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个db.post
操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for
循环(注意map
也是并发的):
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果确实希望多个请求并发执行,可以使用Promise.all
方法。当三个请求都会resolved
时,下面两种写法效果相同:
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
实例:按照顺序完成异步操作:
实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
上面代码中,虽然map
方法的参数是async
函数,但它是并发执行的,因为只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,因此实现了按顺序输出。
其他文章 https://juejin.im/post/5b727f546fb9a009c72caf79
4.10 作用域
作用域也可以看做是一套依据名称查找变量的规则。那我们再细看一下这个规则,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。对应到实际问题来说,就是我们熟悉的函数或者变量可以在什么地方调用。
JS中的作用域类型
- 函数作用域
函数作用域是js中最常见的作用域了,函数作用域给我们最直观的体会就是,内部函数可以调用外部函数中的变量。一层层的函数,很直观的就形成了嵌套的作用域。我们常常听到的“如果在函数内部我们给一个未定义的变量赋值,这个变量会转变为一个全局变量”,我们从对标识符的操作的角度来理解这句话。
var a = 1;
function foo(){
// b第一次出现在函数foo中
b = a ;
}
foo();
// 全局可以访问到b
console.log(b); //1
- 块作用域
除了函数作用域,JS也提供块作用域。我们应该明确,作用域是针对标识符来说的,块作用域把标识符限制在{}
中。
ES6
提供的let
,const
方法声明的标识符都会固定于块中。常被大家忽略的try/catch
的catch
语句也会创建一个块作用域。
- 改变函数作用域的方法
一般说来词法作用域在代码编译阶段就已经确定,这种确定性其实是很有好处的,代码在执行过程中,能够预测在执行过程中如何对它们进行查找。能够提高代码运行阶段的执行效率。不过JS
也提供动态改变作用域的方法。eval()
函数和with
关键字。
eval()
方法:
这个方法接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。
function foo(str,a){
eval(str);//欺骗作用域,词法阶段阶段foo()函数中并没有定义标识符,但是在函数运行阶段却临时定义了一个b;
console.log(a,b);
}
var b = 2;
foo("var b =3;",1);//1,3
// 严格模式下,`eval()`会产生自己的作用域,无法修改所在的作用域
function foo(str){
'use strict';
eval(str);
console.log(a);//ReferenceError: a is not de ned
}
foo('var a =2');
eval()
有时候挺有用,但是性能消耗很大,可能也会带来安全隐患,因此不推荐使用。
with
关键字:
with
通常被当作重复引用同一个对象中的多个属性的快捷方式。
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj" obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a被泄漏到全局作用域上了!
// 执行了LHS查询,不存在就在全局创建了一个。
// with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。
with
也会带来性能的损耗。
- 声明提升
作用域关系到的是标识符的作用范围,而标识符的作用范围和它的声明位置是密切相关的。在js中有一些关键字是专门用来声明标识符的(比如va
r,let
,const
),非匿名函数的定义也会声明标识符。
关于声明也许大家都听说过声明提升一词。我们来分析一下造成声明提升的原因。
我们已经知道引擎会在解释JavaScript
代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来(词法作用域的核心)。
这样的话,声明好像被提到了前面。
值得注意的是每个作用域都会进行提升操作。声明会被提升到所在作用域的顶部。
不过并非所有的声明都会被提升,不同声明提升的权重也不同,具体来说函数声明会被提升,函数表达式不会被提升(就算是有名称的函数表达式也不会提升)。
通过var
定义的变量会提升,而let
和const
进行的声明不会提升。
函数声明和变量声明都会被提升。但是一个值得注意的细节也就是函数会首先被提升,然后才是变量,也就是说如果一个变量声明和一个函数声明同名,那么就算在语句顺序上变量声明在前,该标识符还是会指向相关函数。
如果变量或函数有重复声明以会第一次声明为主。
最后一点需要注意的是:
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
4.11 this指向
- 函数调用
JS
(ES5
)里面有三种函数调用形式:
func(p1, p2)
obj.child.method(p1, p2)
func.call(context, p1, p2) // 先不讲 apply
一定要记住,第三种调用形式,才是正常调用形式,其他两种都是语法糖,可以等价地变为call
形式:
func(p1, p2)等价于 func.call(undefined, p1, p2);
obj.child.method(p1, p2) 等价于 obj.child.method.call(obj.child, p1, p2);
这样,this
就好解释了 this
就是上面 context
。this
是你 call
一个函数时传的 context
,由于你从来不用 call
形式的函数调用,所以你一直不知道。
先看func(p1, p2)
中的 this
如何确定:
当你写下面代码时
function func(){
console.log(this)
}
func()
等价于
function func(){
console.log(this)
}
func.call(undefined) // 可以简写为 func.call()
按理说打印出来的this
应该就是 undefined
了吧,但是浏览器里有一条规则:
如果你传的
context
就null
或者undefined
,那么window
对象就是默认的context
(严格模式下默认context
是undefined
)
因此上面的打印结果是window
。如果你希望这里的this
不是window
,很简单:
func.call(obj) // 那么里面的 this 就是 obj 对象了
所以经常看到的面试题这样解答:
var obj = {
foo: function(){
console.log(this)
}
}
var bar = obj.foo
obj.foo() // 转换为 obj.foo.call(obj),this 就是 obj
bar()
// 转换为 bar.call()
// 由于没有传 context
// 所以 this 就是 undefined
// 最后浏览器给你一个默认的 this —— window 对象
- [] 语法
如下代码:
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 这里面的 this 又是什么呢?
我们可以把 arr[0]( )
想象为arr.0( )
,虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2)
对应上了,于是就可以愉快的转换了:
arr[0]()
假想为 arr.0()
然后转换为 arr.0.call(arr)
那么里面的 this 就是 arr 了
- 小结
this 就是你 call 一个函数时,传入的第一个参数。
如果你的函数调用不是 call 形式, 请将其转换为 call 形式
4.12 闭包(closure)
闭包在JavaScript
中常用来实现对象数据的私有,在事件处理和回调函数中也常常会用到它,此外还有偏函数应用(partial applications
)和柯里化(currying
),以及其他函数式编程模式。
在函数外访问了函数内的标识符,如下面代码:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果。在函数外访问了函数内的标识符
// bar()函数持有对其父作用域的引用,而使得父作用域没有被销毁,这就是闭包
如定义一个模块,采用闭包:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
// 返回的是一个对象,对象中可能包含各种函数
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
// 在外面调用返回对象中的方法就形成了闭包
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
setTimeout
函数之循环和闭包
https://www.jianshu.com/p/e5225ba4a025
- 闭包的缺陷
闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
4.13 原型链
所有的函数都有一个特殊的属性: prototype
(原型),prototype
属性是一个指针,指向的是一个对象(原型对象),原型对象中的方法和属性都可以被函数的实例所共享。所谓的函数实例是指以函数作为构造函数创建的对象,这些对象实例都可以共享构造函数的原型的方法
要知道属性__proto__
是指向的他的构造函数的原型对象,而它的构造函数的原型对象有一个constructor
属性指向的是构造函数本身。
执行上面代码时,首先会在对象实例person
中查找属性toString
方法,我们发现实例中不存在toString
属性。然后我们转到person
内部指针[[Prototype]]
指向的Person
原型对象去寻找toString
属性,结果是仍然不存在。这找不到我们就放弃了?开玩笑,我们这么有毅力。我们会再接着到Person
原型对象的内部指针[[Prototype]]
指向的Object
原型对象中查找,这次我们发现其中确实存在toString
属性,然后我们执行toString方法
。发现了没有,这一连串的原型形成了一条链,这就是原型链。
4.14 几种继承方式
- 原型链实现继承
function Parent () {
this.name = '张三';
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // 张三
存在的问题:
- 引用类型的属性被所有实例共享,举个例子:
function Parent () {
this.names = ['张三', '李四'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('王五');
console.log(child1.names); // ["张三", "李四", "王五"]
var child2 = new Child();
console.log(child2.names); // ["张三", "李四", "王五"]
- 在创建
Child
的实例时,不能向Parent
传参
- 构造函数实现继承
function Parent () {
this.names = ['张三', '李四'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('王五');
console.log(child1.names); // ["张三", "李四", "王五"]
var child2 = new Child();
console.log(child2.names); // ["张三", "李四"]
https://juejin.im/post/5d259684e51d454d56535874
4.15 js 的暂时性死区
在代码块内,使用let
、const
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone
,简称 TDZ
)。
所以说“暂时性死区”也意味着typeof
不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
4.16 ES6 中的 map 对象跟原生的对象之间的区别
object
和Map
存储的都是键值对组合。但是:
object
的键的类型是 字符串;
map
的键的类型是 可以是任意类型;
另外注意,object
获取键值使用Object.keys
(返回数组);
Map
获取键值使用 map变量.keys()
(返回迭代器)。
let a = {
o: 1
};
// string
console.log(typeof Object.keys(a)[0]);
let map = new Map();
map.set(a, 'content');
// 输出是object 也可以是任何类型
console.log(map.keys().next());
优点:
- 避免了引用类型的属性被所有实例共享
- 可以在 Child 中向 Parent 传参
https://juejin.im/post/5d2d51cb51882563453244b6
4.17 ES6 中的 Set 对象
在Set
对象内部,两个NaN
是相等的:
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
另外,两个对象总是不相等的。
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
WeakSet
结构与 Set
类似,也是不重复的值的集合。但是,它与Set
有两个区别。WeakSet
的成员只能是对象,而不能是其他类型的值。
Set
相关的操作有:add、delete、has、size属性
对应map
的操作:set、get、has、delete、size属性
4.18 事件的捕获与冒泡
https://juejin.im/post/5acf3130f265da23a1424a99
4.19 数组与对象的深拷贝
实现代码如下:
// 数组的深拷贝
// slice
let arr = [1, 2, 30]
let arrTo = arr.slice(0)
// concat
let arr1 = [1,2, 3]
let arrTo1 = arr1.concat()
// for of 直接遍历
let arr2 = [1, 2, 30]
function copyArr(arr2) {
let newArray = []
for (let item of arr2) {
newArray.push(item)
}
}
// 对象的深拷贝
// 1. 直接遍历
let obj = {name: 'ddd', job: 'dalao'}
function copyObj(obj) {
let newObj = {}
for (let item in obj) {
newObj[item] = obj[item]
}
return newObj
}
// 2. es6的 Object.assign(注意这里其实不是深拷贝,如果拷贝的源对象中有一个属性为对象,那么它也只指向那个引用。)
let obj1 = { name: 'ddd', job: 'dalao' }
let copyObj = Object.assign({}, obj)
// 下面拷贝将会污染变量
let obj1 = { a: 0 , b: { c: 0}};
let obj2 = Object.assign({}, obj1);
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}
obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}
// 3. 使用JSON.parse以及JSON.stringify方法--但是这种简单粗暴的方法有其局限性。当值为undefined、function、symbol 会在转换过程中被忽略;所以,对象值有这三种的话用这种方法会导致属性丢失。
obj1 = { a: 0 , b: { c: 0}};
let obj3 = JSON.parse(JSON.stringify(obj1));
obj1.a = 4;
obj1.b.c = 4;
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
// 4. 通过函数实现深拷贝
function deepCopy(obj) {
var result = Array.isArray(obj) ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key]!==null) {
result[key] = deepCopy(obj[key]); //递归复制
} else {
result[key] = obj[key];
}
}
}
return result;
}
4.12 实现队列与堆栈
队列:是一种支持先进先出(FIFO
)的集合,即先被插入的数据,先被取出!
堆栈:是一种支持后进先出(LIFO
)的集合,即后被插入的数据,先被取出!
在JavaScript
中实现队列和数组主要是通过数组,js
数组中提供了以下几个方法可以让我们很方便实现队列和堆栈:
shift
:从数组中把第一个元素删除,并返回这个元素的值。
unshift
: 在数组的开头添加一个或更多元素,并返回新的长度
push
:在数组的中末尾添加元素,并返回新的长度
pop
:从数组中把最后一个元素删除,并返回这个元素的值。
- 实现队列
var queue = new Array();
// unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
queue.unshift(1);
queue.unshift(2);
queue.unshift(3);
queue.unshift(4);
// pop() 方法用于删除并返回数组的最后一个元素。
var first = queue.pop();
console.log(first); // 结果为1,先进先出
- 实现堆栈
var stack = new Array();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
var first = stack.pop();
console.log(first);// 结果为4,先进后出
需要知道的是:堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。
javascript
的基本类型就5
种:Undefined
、Null
、Boolean
、Number
和String
,它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。
javascript
中其他类型的数据被称为引用类型的数据 : 如对象(Object
)、数组(Array
)、函数(Function
) …,它们是通过拷贝和new
出来的,这样的数据存储于堆中。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。
5. vue相关
5.1 vue-cli
- 初始化后的
package.js
文件
项目初始化之后,package.js
内容如下:
{
"name": "my-project",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^2.6.5",
"vue": "^2.6.10"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
5.2 vue-router 中 hash 模式与 history 模式的对比
在vue
的路由配置中有mode
选项 最直观的区别就是在url
中hash
带了一个很丑的#
而history
是没有#
的。
vue-router
默认 hash
模式 —— 使用URL
的 hash
来模拟一个完整的 URL
,于是当 URL
改变时,页面不会重新加载。
如果不想要很丑的 hash
,我们可以用路由的 history
模式,这种模式充分利用history.pushState API
来完成 URL
跳转而无须重新加载页面。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
当使用 history
模式时,URL
就像正常的url
,例如 http://yoursite.com/user/id,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,要在服务端增加一个覆盖所有情况的候选资源:如果URL
匹配不到任何静态资源,则应该返回同一个index.html
页面,这个页面就是你 app
依赖的页面。
- 两者的区别:
hash
特点:比如这个URL
:http://www.abc.com/#/hello
,hash
的值为#/hello
,hash
虽然出现在URL
中,但不会被包括在HTTP
请求中,对后端完全没有影响,因此改变hash
不会重新加载页面。history
特点: 利用了HTML5 History Interface
中新增的pushState()
和replaceState()
方法。(需要特定浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的back
、forward
、go
的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的URL
,但浏览器不会立即向后端发送请求。
hash
模式
hash
模式背后的原理是onhashchange
事件,可以在window
对象上监听这个事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
document.body.style.color = hash;
}
history
模式
修改历史状态包括了pushState
,replaceState
,两个方法,这两个方法接收三个参数:stateObj
,title
,url
:
history.pushState({color:'red'}, 'red', 'red')
history.back();
setTimeout(function(){
history.forward();
},0)
window.onpopstate = function(event){
console.log(event.state)
if(event.state && event.state.color === 'red'){
document.body.style.color = 'red';
}
}
通过pushstate
把页面的状态保存在state
对象中,当页面的url
再变回这个url
时,可以通过event.state
取到这个state
对象,从而可以对页面状态进行还原,这里的页面状态就是页面字体颜色,其实滚动条的位置,阅读进度,组件的开关的这些页面状态都可以存储到state
的里面。
通过history api
,我们丢掉了丑陋的#,但是它也有个问题:不怕前进,不怕后退,就怕刷新,f5
,(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的,不玩虚的。
在hash
模式下,前端路由修改的是#
中的信息,而浏览器请求时是不带它玩的,所以没有问题.但是在history
下,你可以自由的修改path
,当刷新时,如果服务器中没有相应的响应或者资源,会分分钟刷出一个404
来。
- 总结
hash
模式下,仅hash
符号之前的内容会被包含在请求中,如http://www.abc.com
,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404
错误。
2.history
模式下,前端的URL
必须和实际向后端发起请求的URL
一致,如http://www.abc.com/book/id
。如果后端缺少对/book/id
的路由处理,将返回404
错误。Vue-Router
官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果URL
匹配不到任何静态资源,则应该返回同一个index.html
页面,这个页面就是你app
依赖的页面。”- 结合自身例子,对于一般的
Vue
+Vue-Router
+Webpack
+XXX
形式的Web
开发场景,用history
模式即可,只需在后端(Apache
或Nginx
)进行简单的路由配置,同时搭配前端路由的404
页面支持。
5.3 组件之间的调用
父组件调用子组件的方法:this.$refs.子组件ref的值.方法名
,或者可以通过调用this.$children
或者到所有子组件的数组,按照调用关系的先后顺序的数组,然后调用子组件的方法。
5.4 对MVVM的理解
MVVM
是Model-View-ViewModel
的缩写,Model
代表数据模型负责业务逻辑和数据封装,View
代表UI
组件负责界面和显示,ViewModel
监听模型数据的改变和控制视图行为,处理用户交互,简单来说就是通过双向数据绑定把View
层和Model
层连接起来。在MVVM
架构下,View
和Model
没有直接联系,而是通过ViewModel
进行交互,我们只关注业务逻辑,不需要手动操作DOM
,不需要关注View
和Model
的同步工作。
5.5 vue等单页面应用及优缺点
vue
核心是一个响应的数据绑定系统,mvvm
,数据驱动,组件化,轻量,简洁,高效,快速,模块友好。
缺点:不支持低版本浏览器,最低到IE9,不利于SEO的优化,首页加载时间较长,不可以使用浏览器的导航按钮需要自行实现前进后退。
5.6 route和router的区别
route
是路由信息对象,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。
router是路由实例对象,包括了路由的跳转方法,钩子函数。
5.7 vue-router的routes中name属性作用详解
我们一般配置路由如下:
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Hello',
component: Hello
}, {
path: '/text',
name: 'text',
component: text
}, {
path: '/text/:id',
component: param
}
]
})
第一种用法:
通过name
属性,为一个页面中不同的router-view
渲染不同的组件,如:将上面代码的Hello
渲染在 name
为Hello
的router-view
中,将text
渲染在name
为text
的router-view
中。不设置name
的将为默认的渲染组件。
<template>
<div id="app">
<router-view></router-view>
<router-view name="Hello"></router-view> //将渲染Hello组件
<router-view name="text"></router-view> //将渲染text组件
</div>
</template>
第二种用法:
可以用name
传参 使用$router.name
获取组件name
值
<template>
<div id="app">
<p>{{ $route.name }}</p> //可以获取到渲染进来的组件的name值
<router-view></router-view>
</div>
</template>
第三种用法:
用于pramas
传参的引入 pramas
必须用name
来引入 query
可以用name
或者path
来引入
链接:https://blog.csdn.net/alokka/article/details/84318734
5.8 vue的声明周期
声明周期
生命周期共分为8
个阶段创建前/后,载入前/后,更新前/后,销毁前/后
创建前/后:在beforeCreated
阶段,vue
实例的挂载元素el
和数据对象data
都为undefined
,还未初始化。created
阶段,vue
实例的数据对象data
有了,el
还没有。
载入前后:在beforeMount
阶段,vue
实例的el
和data
都初始化了,但还是挂载之前为虚拟的dom
节点,data.message
还未替换。在mounted
阶段,vue
实例挂载完成,data.message
成功渲染
更新前/后:当data
变化时,会触发beforeUpdated
和updated
方法
销毁前/后:beforeDestroy
在实例销毁前调用,实例仍然完全可用。destroy
在实例销毁之后调用,调用后所有事件监听器会被移除,所有子实例也会被销毁。
生命周期的作用?
生命周期中有多个事件钩子,让我们在控制整个Vue
实例的过程中更容易形成好的逻辑。
5.9 vue如何自定义一个过滤器
<input type="text" v-model="msg" />
{{msg | capitalize}}
data(){
return{
msg: ''
}
},
filters: {
capitalize: function(value){
if(!value) return "";
value = value.toString();
return value.charAt(0).toUpperCase()+value.slice(1)
}
}
5.10 computed和watch区别
computed
是计算属性,依赖其他属性计算值,并且computed
的值有缓存,只有当计算值变化才会返回内容。
watch
监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
一般来说需要依赖别的属性来动态获得值的时候可以使用computed
,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用watch
另外computed
和watch
还支持对象的写法
data: {
firstName: 'Chen',
lastName: 'Miao',
fullName: 'Chen Miao'
},
watch: {
firstName: function(val){
this.fullName = val+ ' '+this.lastName
},
lastName: function(val){
this.fullName = this.firstName+ ' '+val
}
},
computed: {
anoFullName: function(){
return this.firstName+' '+this.lastName
}
}
5.11 extend能做什么
作用是扩展组件生成一个构造器,通常与$mount
一起使用。
// 创建组件构造器
let Component = Vue.extend({
template: '<div>test</div>'
})
// 挂载到#app上
new Component().$mount('#app')
// 扩展已有组件
let SuperComponent = Vue.extend(Component)
new SuperComponent({
created(){
console.log(1)
}
})
new SuperComponent().$mount('#app')
5.12 mixin和mixins区别
mixin
用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
Vue.mixin({
beforeCreate(){
// 会影响到每个组件的beforeCreate钩子函数
}
})
mixins
最常用的扩展组件的方式。如果多个组件有相同的业务逻辑,就可将这些逻辑剥离出来,通过mixins
混入代码。需要注意:mixins
混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
在组件中进行混入如下:
import { isShowFooter } from '../../../mixins/isShowFooter.js'
import { navbar } from '../../../mixins/navbar.js'
import api from '@/api/index'
export default {
mixins: [isShowFooter, navbar],
isShowFooter.js
中的内容:
import { mapMutations } from 'vuex'
export const isShowFooter = {
data() {
return {}
},
created() {
this.initState()
},
methods: {
...mapMutations(['footerIsShow']),
initState() {
this.footerIsShow(false)
}
}
}
这里是通过控制一个state
来判断是否显示组件;比如上面的代码,要显示底部的工具条,在布局的组件里面直接调用底部工具组件:
<template>
<div>
<router-view></router-view>
<Footer v-show="show"></Footer>
</div>
</template>
<script>
import Footer from '../../components/footer/Footer'
import { mapState, mapMutations } from 'vuex'
export default {
components: {
Footer
},
computed: {
...mapState({
show: state => state.index.show
})
},
watch: {
'$route': function (to, from) {
const navListPath = ['/work', '/info', '/my', '/address'];
const isIncludePath = navListPath.some((nav) => {
return nav === to.path
})
// 共用组件时 刷新问题
if (isIncludePath) {
this.footerIsShow('true')
}
}
},
mounted () {
},
methods: {
...mapMutations(['footerIsShow'])
}
}
</script>
<style lang="scss" scoped>
</style>
5.13 如何使用vue.nextTick()
nextTick
可以使我们在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
。
data:function(){
return {
message: '没有更新'
}
},
methods: {
updateMessage: function(){
this.message='更新完成'
console.log(this.$el.textContent) // '没有更新'
this.$nextTick(function(){
console.log(this.$el.textContent)// '更新完成'
})
}
}
5.14 transition 过渡的实现原理
<transition name="fade1">
<router-view></router-view>
</transition>
类名介绍:
v-enter:定义进入过渡的开始状态
v-enter-active:定义进入过渡生效时的状态
v-enter:定义进入过渡的结束状态
v-leave:定义离开过渡的开始状态
v-leave-active:定义离开过渡生效时的状态
v-leave-to:定义离开过渡的结束状态
5.15 非父子组件通讯
- 新建一个
bus.js
文件:
import Vue from 'vue';
export default new Vue();
- 使用它
<div @click="addCart">添加</div>
import Bus from 'bus.js';
export default{
methods: {
addCart(event){
Bus.$emit('getTarget', event.target)
}
}
}
// 另一组件
export default{
created(){
Bus.$on('getTarget', target =>{
console.log(target)
})
}
}
5.16 vue-Router
普通路由
router.push('home')
router.push({path: 'home')
命名路由
const router=new VueRouter({
routes: [{
path: '/user',
name: 'user',
component: User
}]
})
<router-link :to="{name: 'user'}"></router-link>
router.push({
name: 'user'
})
5.17 vuex中的mapState、mapMutation、mapActions
在子组件中使用mapState
辅助函数,使用最想扩展运算符,如下代码:
computed: {
...mapState({
show: state => state.index.show
})
},
其中show
是index
模块中的state
中存储的值。
如果不使用对象扩展运算符:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
如果是mapMutation
:
methods: {
...mapMutations(['footerIsShow'])
}
mapActions
的如下代码:
methods: {
...mapMutations(['isFooterShow']),
...mapActions(['login']),
}
5.18 vue的双向数据绑定原理的理解
vue.js
是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.definePorperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
MVVM
作为数据绑定的入口,整合Observer
,Compile
和Watcher
三者,通过Observer
来监听自己的model
数据变化,通过Compile
来解析编译模板指定(解析{{}}),最终利用Watcher
搭起Observer
和Compile
之间的通信桥梁,达到数据变化->视图更新;视图交互变化input
->数据model
变更的双向绑定效果
实现简单的双向绑定
<input type="text" id="inp" />
<div id="show"></div>
<script type="text/javascript">
var inp = document.getElementById('inp');
var show = document.getElementById('show');
var obj = {};
function watch(obj, key, callback){
var val = obj[key];
Object.defineProperty(obj, key, {
get: function(){
return val;
},
set: function(newVal){
callback(newVal, this)
}
})
}
watch(obj, "input", function(val){
show.innerHTML = val
})
inp.addEventListener('keyup', function(e){
obj.input = e.target.value
})
</script>
6. 数据结构
6.1 数组与链表的区别
- 什么是链表
链表是一种上一个元素的引用指向下一个元素的存储结构,链表通过指针来连接元素与元素;
链表是线性表的一种,所谓的线性表包含顺序线性表和链表,顺序线性表是用数组实现的,在内存中有顺序排列,通过改变数组大小实现。而链表不是用顺序实现的,用指针实现,在内存中不连续
。意思就是说,链表就是将一系列不连续的内存联系起来,将那种碎片内存进行合理的利用,解决空间的问题。所以,链表允许插入和删除表上任意位置上的节点,但是不允许随即存取。链表有很多种不同的类型:单向链表、双向链表及循环链表
- 单向链表
单向链表包含两个域,一个是信息域,一个是指针域。也就是单向链表的节点被分成两部分,一部分是保存或显示关于节点的信息,第二部分存储下一个节点的地址,而最后一个节点则指向一个空值。
- 双向链表
从上图可以很清晰的看出,每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。意思就是说双向链表有2个指针,一个是指向前一个节点的指针,另一个则指向后一个节点的指针。
- 循环链表
循环链表就是首节点和末节点被连接在一起。循环链表中第一个节点之前就是最后一个节点,反之亦然。
- 数组和链表的区别
不同:链表是链式的存储结构;数组是顺序的存储结构。
链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难
;
数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便
。
相同:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。
- 链表的应用、代码实践
约瑟夫问题
传说在公园1世纪的犹太战争中,犹太约瑟夫是公元一世纪著名的历史学家。在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人俘虏,于是决定了一个流传千古的自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报到第3人该人就必须自杀,然后再由下一个人重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从这个约定,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第_个和第_个位置,于是逃过了这场死亡游戏,你知道安排在了第几个嘛?
//节点类
function Node(elemnt) {
this.item = elemnt;
this.next = null;
}
//循环列表需要修改一下构造函数,和遍历时候的判断条件
//构造函数如下;希望从后向前遍历,又不想要建立双向链表,就使用循环链表。
function Llist() {
this.head = new Node("1");
this.head.next = this.head;
this.remove = remove;
this.insert = insert;
this.find = find;
this.display = display;
//..........
}
function find(number) {
var curr = this.head;
while (curr.item != number) {
curr = curr.next;
}
return curr;
}
function insert(element, newElement) {
var preNode = this.find(element);
var current = new Node(newElement);
current.next = preNode.next;
preNode.next = current;
}
function remove() {
var current = this.head;
console.log("remove");
//跳过两个,杀死一个
while(current.next.next != null && current.item!=current.next.next.item){
var temp = current.next.next;
current.next.next = temp.next;
current = temp.next;
temp.next = null;
}
return current;
}
function display(flag,current) {
var crr = this.head;
if(flag){
while(crr.next.item!="1"){
console.log(crr.item);
crr=crr.next;
}
}else{ //最后只剩两个直接输出
console.log(current.item);
console.log(current.next.item);
}
}
var Clist = new Llist();
//输入排序
for (var i = 1; i < 41; i++){
Clist.insert(i.toString(),(i + 1).toString());
}
//先输出所有
Clist.display(1,null);
//通过remove返回最后杀死后的结果其中一个节点
Clist.display(0,Clist.remove()); //16,31
摘自原文链接:https://blog.csdn.net/m0_37631322/article/details/81777855
其他文章:https://zhuanlan.zhihu.com/p/71625297
6.2 堆与栈
- 栈的定义
- 后进者先出,先进者后出,简称 后进先出(
LIFO
),这就是典型的栈结构。 - 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底。
- 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
- 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。
- 不包含任何元素的栈称为空栈。
栈也被用在编程语言的编译器和内存中保存变量、方法调用等,比如函数的调用栈。
- 栈的实现(方法)
push
(element
):添加一个(或几个)新元素到栈顶。pop()
:移除栈顶的元素,同时返回被移除的元素。peek()
:返回栈顶的元素,不对栈做任何修改。isEmpty()
:如果栈里没有任何元素就返回 true,否则返回 false。clear()
:移除栈里的所有元素。size()
:返回栈里的元素个数。
// Stack类
function Stack() {
this.items = [];
// 添加新元素到栈顶
this.push = function(element) {
this.items.push(element);
};
// 移除栈顶元素,同时返回被移除的元素
this.pop = function() {
return this.items.pop();
};
// 查看栈顶元素
this.peek = function() {
return this.items[this.items.length - 1];
};
// 判断是否为空栈
this.isEmpty = function() {
return this.items.length === 0;
};
// 清空栈
this.clear = function() {
this.items = [];
};
// 查询栈的长度
this.size = function() {
return this.items.length;
};
// 打印栈里的元素
this.print = function() {
console.log(this.items.toString());
};
}
- 测试
// 创建Stack实例
var stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5); // undefined
stack.push(8); // undefined
console.log(stack.peek()); // 8
stack.push(11); // undefined
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
stack.push(15); // undefined
stack.pop(); // 15
console.log(stack.size()); // 3
stack.print(); // 5,8,11
stack.clear(); // undefined
console.log(stack.size()); // 0
- 栈的应用实例:实现一个前端路由以及函数调用栈(在其他模块)
7. 其他
7.1 es5 实现定义常量
这里涉及到了一个方法,Object.defineProperty()
,该方法是ES5
规范中的,该方法的作用是在对象上定义一个新属性,或者修改对象的一个现有属性,并对该属性加以描述,返回这个对象,我们来看一下浏览器兼容性:
特性 | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
基本支持 | 4.0 (2) | 5 | 9 [1] | 11.60 | 5.1 [2] |
IE8
是不支持的,不过IE8
也对该方法进行了实现,只能在DOM对象上适用,而且有一些独特的地方。
Object.defineProperty()
方法可以定义对象属性的数据描述和存储描述,这里我们只讲数据描述符,不对存储描述符讲解,数据描述符有以下选项:
configurable
当且仅当该属性的configurable
为true
时,该属性的描述符(enumerable
、value
、writable
)才能够被改变,也能够被删除。默认为false
。
enumerable
当且仅当该属性的enumerable
为true
时,该属性才能够出现在对象的枚举属性中。默认为false
。
value
该属性对应的值。可以是任何有效的JavaScript
值(数值,对象,函数等)。默认为undefined
。
writable
当且仅当该属性的writable
为true
时,该属性才能被赋值运算符改变。默认为false
。
注意,当我们用常规方法定义属性的时候,其除 value
以外的数据描述符默认均为true
,当我们用 Object.defineProperty()
定义属性的时候,默认为 false
。
也就是说,当我们把writable
设置为 false 的时候,该属性是只读的,也就满足了常量了性质,我们把常量封装在CONST
命名空间里面:
var CONST = {};
Object.defineProperty(CONST, "A", {
value: 1,
writable: false, //设置属性只读
configurable: true,
enumerable: true
});
console.log(CONST.A); //1
CONST.A = 2; //在严格模式下会抛错,在非严格模式下静默失败,修改无效。
可以对一个变量定义常量:
//const cconst=1; 这可以用下面的这行模仿
Object.defineProperty(window, "cconst", {writable: false, value: 1});
//然后我们尝试修改看看
cconst= 3;
console.log(cconst); //发现输出是1,发现并改不了
或者可以通过Object.defineProperty
来设置setter
和getter
函数来实现,封装成一个setConst
函数:
function setConst(name, value) {
Object.defineProperty(window, name, {
set(x) {
throw new Error(`Assignment to constant variable ${name}`);
},
get() {
return value;
}
})
}
setConst('name','jack');
name = 'rose';
//Uncaught Error: Assignment to constant variable name
https://www.cnblogs.com/dong-xu/p/6239199.html
https://segmentfault.com/a/1190000016344599
https://www.zhihu.com/question/277800597
https://www.cnblogs.com/chen-cong/p/7862832.html
7.3 JavaScript实现前端路由
实现是有两种方法:一种是 在数组后面进行增加与删除,另外一种是 利用栈的后进先出原理。
- 在数组最后进行 增加与删除
通过监听路由的变化事件 hashchange
,与路由的第一次加载事件load
,判断如下情况:
url
存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。url
不存在于浏览记录中即为前进,前进时,往数组里面push
当前的路由。url
在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。
另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A
),所以给每个路由添加一个key
值来区分相同路由的不同实例。
注意:这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。
实现代码:
// 路由构造函数
function Router() {
this.routes = {}; //保存注册的所有路由
this.routerViewId = "#routerView"; // 路由挂载点
this.stackPages = true; // 多级页面缓存
this.history = []; // 路由历史
}
Router.prototype = {
init: function(config) {
var self = this;
//页面首次加载 匹配路由
window.addEventListener('load', function(event) {
// console.log('load', event);
self.historyChange(event)
}, false)
//路由切换
window.addEventListener('hashchange', function(event) {
// console.log('hashchange', event);
self.historyChange(event)
}, false)
},
// 路由历史纪录变化
historyChange: function(event) {
var currentHash = util.getParamsUrl();
var nameStr = "router-history"
this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []
var back = false, // 后退
refresh = false, // 刷新
forward = false, // 前进
index = 0,
len = this.history.length;
// 比较当前路由的状态,得出是后退、前进、刷新的状态。
for (var i = 0; i < len; i++) {
var h = this.history[i];
if (h.hash === currentHash.path && h.key === currentHash.query.key) {
index = i
if (i === len - 1) {
refresh = true
} else {
back = true
}
break;
} else {
forward = true
}
}
if (back) {
// 后退,把历史纪录的最后一项删除
this.historyFlag = 'back'
this.history.length = index + 1
} else if (refresh) {
// 刷新,不做其他操作
this.historyFlag = 'refresh'
} else {
// 前进,添加一条历史纪录
this.historyFlag = 'forward'
var item = {
key: currentHash.query.key,
hash: currentHash.path,
query: currentHash.query
}
this.history.push(item)
}
// 如果不需要页面缓存功能,每次都是刷新操作
if (!this.stackPages) {
this.historyFlag = 'forward'
}
window.sessionStorage[nameStr] = JSON.stringify(this.history)
},
}
- 利用栈的 后进者先出,先进者后出 原理
首先javascript
实现一个顺序栈:
// 基于数组实现的顺序栈
class ArrayStack {
constructor(n) {
this.items = []; // 数组
this.count = 0; // 栈中元素个数
this.n = n; // 栈的大小
}
// 入栈操作
push(item) {
// 数组空间不够了,直接返回 false,入栈失败。
if (this.count === this.n) return false;
// 将 item 放到下标为 count 的位置,并且 count 加一
this.items[this.count] = item;
++this.count;
return true;
}
// 出栈操作
pop() {
// 栈为空,则直接返回 null
if (this.count == 0) return null;
// 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一
let tmp = items[this.count-1];
--this.count;
return tmp;
}
}
- 栈的经典应用: 函数调用栈
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。
function add(x, y) {
let sum = 0;
sum = x + y;
return sum;
}
function main() {
let a = 1;
let ret = 0;
let res = 0;
ret = add(3, 5);
res = a + ret;
console.log("res: ", res);
reuturn 0;
}
main();
上面代码也很简单,就是执行main
函数求和,main
函数里面又调用了add
函数,先调用的先进入栈。
执行过程如下:
- 用两个栈实现浏览器的前进、后退功能。
我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。
比如你顺序查看了a
,b
,c
三个页面,我们就依次把 a
,b
,c
压入栈,这个时候,两个栈的数据如下:
当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:
这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:
这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:
其实就是在第一个方法的代码里面, 添加多一份路由历史纪录的数组即可,对这两份历史纪录的操作如上面示例图所示即可,也就是对数组的增加和删除操作而已。
7.2 const 声明的变量不可以改变,声明的对象属性为何可以改变
因为对象是引用类型的,使用const
定义的变量中保存的仅是对象的指针,这就意味着,const
仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const
定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。
const
声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容(例如,其参数)。
7.3 jQuery 与 vue之间的对比
jquery
是以操作dom
为主,做了数据处理之后还需要对dom
进行操作。vue.js
是以操作数据为主,不操作dom
,也就是传说中的双向数据绑定,你只需要操作数据就好,dom
自动更新。这只是对初学者来说最大的不同。jquery
只是一个类库,只是提供了很多的方法,不能算框架,而vue.js
是一个框架,有一套完整的体系。
7.4 从输入URL到看到页面发生的全过程
总体来看主要是下面的流程:
①、浏览器构建HTTP Request请求
②、网络传输
③、务器构建HTTP Response 响应
④、网络传输
⑤浏览器渲染页面
1、首先浏览器主进程接管,开了一个下载线程。
2、然后进行HTTP请求(DNS查询、IP寻址等等),中间会有三次捂手,等待响应,开始下载响应报文。
3、将下载完的内容转交给Renderer进程管理。
4、Renderer进程开始解析css rule tree和dom tree,这两个过程是并行的,所以一般我会把link标签放在页面顶部。
5、解析绘制过程中,当浏览器遇到link标签或者script、img等标签,浏览器会去下载这些内容,遇到时候缓存的使用缓存,不适用缓存的重新下载资源。
6、css rule tree和dom tree生成完了之后,开始合成render tree,这个时候浏览器会进行layout,开始计算每一个节点的位置,然后进行绘制。
绘制结束后,关闭TCP连接,过程有四次挥手。
7.5 三次握手,四次挥手
7.6 数组去重
- 可以使用
set
对象的特性进行去重:
// 去除数组的重复成员
[...new Set(array)]
上面的方法也可以用于,去除字符串里面的重复字符。
[...new Set('ababbc')].join('')
// "abc"
7.8 原生Js无缝滚动效果的简单实现
无缝滚动,前提条件是子box的高度要大于父box的高度,这样才有必要去滚动,否则直接展示就可以了。增添了鼠标移入停止和移出继续滚动的效果。增加了间歇性无缝滚动效果。
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="html"><!--
Time:2016.8.5
Author:Joel
1.html标签<marquee>,可以考察一下在html5标准下使用什么来实现
可以设置滚动区域的宽高
behavior = “alternate” 两端之间来回滚动
behavior = “scroll” 由一端到另一端,重复滚动
behavior = “slide” 由一端到另一端,不重复
direction 滚动的方向(down,up,left,right)
loop (滚动的次数,loop = -1 表示无限滚动,默认为-1)
scrollamount 设置活动字幕的滚动速度
scrolldelay 设置活动字幕两次滚动之间的延迟时间
2.onmouseover = "this.stop()"
3.onmouseout = "this.start()"
4.父元素设置高度 滚动元素也需要设置高度 而且滚动元素的高度必须大于父元素 这样才能滚动
5.setTimeout(表达式,延迟时间) 仅执行1次
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>InfoRoll</title>
<style>
.box{
width:300px;
border: 1px solid #8f8f8f;
}
dl dt{
text-align: center;
margin-bottom: 20px;
}
.box1{
height:150px;
width:200px;
border:1px solid #8f8f8f;
margin: 0 auto;
overflow: hidden;
}
ul{
height: 200px;
}
ul li{
height: 24px;
}
p{
text-align: center;
}
</style>
<script type="text/javascript">
window.onload = function(){
//无缝滚动效果
// var box1 = document.getElementsByClassName("box1");
// var com1 = document.getElementsByClassName("com1");
// var com2 = document.getElementsByClassName("com2");
// com2[0].innerHTML = com1[0].innerHTML;
// var myscr = function(){
// if(box1[0].scrollTop >= com1[0].offsetHeight){
// box1[0].scrollTop = 0;
// }
// else{
// box1[0].scrollTop++;
// }
// }
// //这里出现了很诡异的问题。如果使用function myscr(){},传入"myscr()"会一直报错,不知道是什么问题。
// var timer = setInterval(myscr,50);
// box1[0].onmouseover = function(){
// clearInterval(timer);
// }
// box1[0].onmouseout = function(){
// timer = setInterval(myscr,50);
// }
//间歇性滚动效果
var box1 = document.getElementsByClassName("box1");
box1[0].scrollTop = 0;
var liHeight = 24;
var timer;
function startMove(){
box1[0].scrollTop++;
timer = setInterval(myscr,50);
}
function myscr(){
if(box1[0].scrollTop % liHeight == 0){
clearInterval(timer);
setTimeout(startMove,2000);
}
else{
box1[0].scrollTop++;
if(box1[0].scrollTop >= box1[0].scrollHeight / 2){
box1[0].scrollTop = 0;
}
}
}
setTimeout(startMove,2000);
}
</script>
</head>
<body>
<!--几种滚动实例-->
<!-- <section class = "box">
<dl>
<dt>默认滚动</dt>
<dd><marquee>默认滚动</marquee></dd>
</dl>
<dl>
<dt>文字滚动(向右)</dt>
<dd><marquee direction="right">向右走...</marquee></dd>
</dl>
<dl>
<dt>文字滚动(来回滚动)</dt>
<dd><marquee behavior = "alternate" loop = "-1" scrollamount = "20">来回滚动...</marquee></dd>
</dl>
<dl>
<dt>向上滚....</dt>
<dd><marquee direction="up" width = "300px" height = "100px">向上走起...</marquee></dd>
</dl>
</section> -->
<!--实现无缝滚动效果-->
<div class = "box1">
<ul class = "com1">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
<ul class = "com2">
</ul>
</div>
</body>
</html>
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">
</span>
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">小结:</span>
https://blog.csdn.net/my_coding2015/article/details/52133364
7.9 如何实现一个弹幕
7.10 js数组的堆与栈实现
7.11 4. 如果有成千上万个菜单,如何绑定点击事件
第一种:for
循环,会大大消耗性能:
//获取所有li的节点
var list = document.getElementsByTagName("li");
//给每个li绑定事件
for(var i = 0;i<list.length;i++){
list[i].ondblclick = function(){
//弹出对应的li节点里面的内容
alert(this.innerHTML);
this.style.color = "red";
}
或者循环中使用立即执行函数来实现。
var ulInLis = document.getElementById("ul").getElementsByTagName("li");
for ( var i = 0; i < ulInLis.length; i++) {
(function() {
var l = i;
ulInLis[l].onclick = function() {
console.log(l);
console.log(ulInLis[l]);
}
})();
}
第二种:在父级通过addEventListener
绑定点击事件,并阻止事件冒泡:
let ulDom = document.getElementsByTagName('ul')
ulDom[0].addEventListener('click', (dom) => {
// dom.preventDefault()
dom.target.onclick = (de) => {
alert(de.target.innerText)
}
}, true)
8 css 相关
8.1 实现元素的水平垂直居中
第一种:定位+负边距
.box{
position:absolute;
left:50%;
top:50%;
margin-left:-150px;
margin-top:-100px;
padding:20px;
width:300px;
height:200px;
background:#41D7FB;
}
第二种:定位+自适应边距
.box{
position:absolute;
left:0;
right:0;
top:0;
bottom:0;
margin:auto;
padding:20px;
width:300px;
height:200px;
background:#41D7FB;
}
第三种: 定位+CSS3
位移
.box{
position:absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
padding:20px;
width:300px;
height:200px;
background:#41D7FB;
}
第四种:Flex
布局实现
html{
display: flex;
height: 100%;
justify-content: center;
align-items:center;
}
.box{
padding:20px;
width:300px;
height:200px;
background:#41D7FB;
}
第五种:table-cell
配合inline-block
.table{
display:table;
width:100%;
height:600px;
}
.table-row{
display: table-row;
}
.table-cell{
display: table-cell;
text-align: center;
vertical-align: middle;
}
.box{
display: inline-block;
padding:20px;
width:300px;
height:200px;
background:#41D7FB;
}
8.2 用伪类清除浮动
之前我们一般使用
<div sytle="clear:both"></div>
来解决,但这样会增加无语义的标签,下面是用after伪类实现,兼容多种浏览器:
.clearfix:after{
content:".";
display:block;
height:0;
clear:both;
visibility:hidden;
}
为兼容IE6
,IE7
,因为ie6
,ie7
不能用after
伪类。加上下面代码:
.clearfix{zoom:1}
9 git
创建分支:git branch b1
切换分支:git checkout b1