参考部分网上的文章,总结笔记
一. JavaScript篇
1. 数据类型
JavaScript有几种数据类型:
- 基本数据类型: undefine/null/ boolean/ number/string/symbol(es6的新数据类型)
- 引用数据类型: object/array/function (统称为object)
数据类型检测:
- typeof : 对于基本数据类型,除了null外,都可以正常检测出数据类型,对于对象来说,除了function外,都会显示object
typeof 5 // 'number'
typeof '5' // 'string'
typeof undefined // 'undefined'
typeof false// 'boolean'
typeof Symbol() // 'symbol'
typeof null //object
typeof NaN //number
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
- instanceof: 通过原型链来判断数据类型的
p1 = new Person()
p1 instanceof Person // true
- Object.prototype.toString.call()可以检测所有的数据类型,算是一个比较完美的方法了。
var obj={}
var arr=[]
console.log(Object.prototype.toString.call(obj)) //[object Object]
console.log(Object.prototype.toString.call(arr)) //[object Array]
2. 拷贝
对于一维对象来说,拷贝没有区别,但是对与多维度对象来说,拷贝的只是一维的数据,多为的数据并没有真正的拷贝成功。实际上,浅拷贝只拷贝一次指针,并没有拷贝一份内存地址
- 浅拷贝: Object.assign() //es6的方法
var obj={aa:1,b:{item:'45'}};
var newObj=Object.assign({},obj);
obj.aa=2;
obj.b.item='kk';
console.log(newObj.aa); //1
console.log(newObj.b.item); //kk
当原对象的数据发生变化后,拷贝的对象也发生了改变,一维度的数据其实实现了深拷贝,但是多维度的数据没有实现
- 深拷贝: JSON.parse(JSON.stringify(obj))
利用JSON.stringify(obj)将对象先转为json字符串,再JSON.parse()转回为json对象可以实现深拷贝,这也是比较常用的一种方法
- 用js实现一个深拷贝函数:
其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。
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;
}
3. 作用域和闭包
- 变量名提升:
-
在 JavaScript 中,函数声明(functionaa(){})与变量声明(var)经常被 JavaScript 引擎隐式地提升到当前作用域的顶部。
-
函数声明的优先级高于变量,如果变量名跟函数名相同且未赋值,则函数声明会覆盖变量声明
-
声明语句中的赋值部分并不会被提升,只有变量的名称被提升
- 作用域链: 因为函数的嵌套后,作用域就有了层级关系,当函数执行的时候,就会在当前的作于环境下查找对应的变量,当当前的作用域下没有这个变量,就会向上级作用域查找,直到全局作用域下,这个动作被称为作用域链。
==注意: 全局作用域下无法获取局部作用域的变量,但是局部作用域下可以访问全局作用域的变量==。
闭包(Closure):
什么是闭包: 简而言之就是一个函数可以访问其他函数内部的变量,就是这个函数和这个变量组成的闭包。
闭包的作用: 隐藏一个变量,让一个变量不能直接被访问,也即使将变量放在一个函数内部。
那么问题来了,由于作用域的关系,全局作用域下无法访问到函数内部的作用域的变量,为解决这一问题,就出现了闭包。
function foo(){
var local = 1
function bar(){ // bar()函数和变量local组成了一个闭包
local++
return local
}
return bar // 注意 这里返回的是一个函数名,而不是一个函数
}
var func = foo()
console.log(func()) // 2
console.log(func()) // 3
闭包的优缺点:
优点:
- 可以让外部函数访问到另一个函数内部的变量
- 让一个变量长期保存在内存中,是的变量的生命周期变长
缺点:
- 因为变量可以长期存在在内存中,无疑消耗了内存
- 在ie浏览器中还会导致内存泄漏
4. 原型和继承
什么是原型: 在每一个js实例对象被创建的时候,都会关联到另一个对象,这个对象我们称之为 原型。实例对象在被创建的时候会继承原型中的属性。
JS创建一个对象的方式:
- 对象字面量方式:
var obj = {}
- new一个构造函数
function Pel() { }
var p = new Pel();
p.name = "hu";
p.age = "25";
delete p.name; // 删除一个属性
p.address = function () {}
console.log(p)// {name: "hu", age: "25", address: ƒ}
JS实现一个类:
- 构造函数法:
缺点:用到了 this 和 prototype,编写复杂,可读性差
function P(name, age){
this.name = name;
this.age= age;
}
P.prototype.sal= function(){
}
var pel= new P("jj", 1);
pel.sell()
- ES6 语法糖 class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
原型链:
- 什么是原型链:
遍历一个实列的属性时,先遍历实列对象上的属性,再遍历它的原型对象,一直遍历到Object
任何一个类(函数)都有原型对象,原型对象至少有两个属性(constructor,proto)。constructor指向函数本身,proto指向父类原型对象。
函数上有一个prototype属性,指向原型对象,通过它可以访问原型对象
function Dog() { } //类
var obj = new Dog(); //实列
obj.name = '沪江';
Dog.prototype.name = "旺财";
Dog.prototype.eat = function () {
console.log(this.name);
};
console.log(Dog.prototype.name); //旺财
console.log(obj.prototype); //undefined,prototype是类上才有的,实列上没有
console.log(obj.__proto__); //{name: "旺财", eat: ƒ, constructor: ƒ}
obj.eat(); //沪江(先遍历实列对象上的属性,再遍历它的原型对象)
Js如何实现继承?
- 构造函数绑定:使用 call 或 apply 方法,将父对象的构造函数绑定在子对象上
function Cat(name,color){
 Animal.apply(this, arguments);
 this.name = name;
 this.color = color;
}
- 实例继承:将子对象的 prototype 指向父对象的一个实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
- 拷贝继承:如果把父对象的所有属性和方法,拷贝进子对象
function extend(Child, Parent) {
   var p = Parent.prototype;
   var c = Child.prototype;
   for (var i in p) {
    c[i] = p[i];
   }
   c.uber = p;
}
- 原型继承:将子对象的 prototype 指向父对象的 prototype
function extend(Child, Parent) {
var F = function(){};
 F.prototype = Parent.prototype;
 Child.prototype = new F();
 Child.prototype.constructor = Child;
 Child.uber = Parent.prototype;
}
- ES6 语法糖 extends继承
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
5. new和this
当我们new一个数据的时候,new操作符到底做了什么?
- 创建一个实例对象
- 使this指向了这个实例对象
- 继承了构造函数的原型,以及原型的属性
如何改变this的指向
- apply
apply方法让我们构建一个参数数组给调用函数,它也允许我们选择this的值,apply接受两个参数,第一个是要绑定给this的值,第二个就是一个参数数组。当第一个参数为null、undefined的时候,默认指向window
var arr = [1,2,3,89,46]
var max = Math.max.apply(null,arr)//89
- 举一个例子:
var xw={
name: "小王",
gender: "男",
age: 24,
say: function(){
alert(this.name+" , "+this.gender+" ,今年"+this.age);
}
}
var xh={
name: "小红",
gender: "女",
age: 18
}
xw.say(); // 小王 , 男 , 今年24
那么如何用xw的say方法来显示xh的数据呢
// 将this的指向了xh
xw.say.call(xh);
xw.say.apply(xh);
xw.say.bind(xh)();
call 和 apply的用法相似,不同的是call的其它参数是一个个单个变量,而 apply的第二个参数可以是一个数组
function fruits() {}
fruits.prototype = {
color: 'red',
say: function() {
console.log(this.color);
}
};
var apple = new fruits();
apple.say(); // red, 此时方法里面的this 指的是fruits
banana = {color: 'yellow'};
apple.say.call(banana); //yellow,此时的this的指向已经通过call()方法改变了,指向的是banana,this.color就是banana.color='yellow';
apple.say.apply(banana);//yellow,同理,此时的this的指向已经通过apply()方法改变了,指向的是banana,this.color就是banana.color ='yellow';
apple.say.apply(null); //undefined, null是window下的,此时,this 就指向了window ,但是window下并没有clolr这个属性,因此this.clolr就是window.color=undefined;
6. promise、generator、async/await怎么使用,有什么区别?
众所周知,js是一门单线程语言,如果没有异步编程,那就会导致程序卡死。
promise、generator、async/await就是异步编程的三种解决方案。
- 回调函数
在以上三种出现之前,js通过回调函数来时先异步操作;所谓回调函数(callback),就是把任务分成两步完成,第二步单独写在一个函数里面,等到重新执行这个任务时,就直接调用这个函数。并且容易出现回调地狱
- promise:
假定我们有一个需求,读取完A文件之后读取B文件,再读取C文件,代码如下:
fs.readFile(fileA, (err, data) => {
fs.readFile(fileB, (err, data) => {
fs.readFile(fileC, (err,data)=>{
//do something
})
});
});
primise不是一种新的功能,而是一种新的写法:
function doIt() {
console.time('do it now')
const time1 = 300;
step1(time1)
.then( time2 =>step2(time2))
.then( time3 => step3(time3))
.then( result => {
console.log(`result is ${result}`)
});
}
doIt();
- Generator函数:
Generator是协程在ES6的实现,最大的特点就是可以交出函数的执行权,懂得退让。
function* gen(x) {
var y = yield x +2;
return y;
}
var g = gen(1);
console.log( g.next()) // { value: 3, done: false }
console.log( g.next()) // { value: undefined, done: true }
上面代码中,函数多了*号,用来表示这是一个Generator函数,和普通函数不一样,不同之处在于执行它不会返回结果,
返回的是指针对象g,这个指针g有个next方法,调用它会执行异步任务的第一步。 对象中有两个值,value和done,value 属性是 yield 语句后面表达式的值,表示当前阶段的值,done表示是否Generator函数是否执行完毕
下面看看Generator函数如何执行一个真实的异步任务:
var fetch = require('node-fetch')
function* gen(){
var url = 'https://api.github.com/users/github'
var result = yield fetch(url)
console.log(result.bio)
}
var g = gen()
var result = g.next()
result.value.then( data => return data.json).then (data => g.next(data))
- Async/await
从回调函数,到Promise对象,再到Generator函数,JavaScript异步编程解决方案历程可谓辛酸,终于到了Async/await。很多人认为它是异步操作的最终解决方案
其实async函数就是Generator函数的语法糖,例如下面两个代码:
var gen = function* (){
var f1 = yield readFile('./a.txt')
var f2 = yield readFile('./b.txt')
console.log(f1.toString())
console.log(f2.toString())
}
var asyncReadFile = async function (){
var f1 = await readFile('./a.txt')
var f2 = await readFile('./b.txt')
console.log(f1.toString())
console.log(f2.toString())
}
async function doIt() {
console.time("doIt")
const time1 = 300;
const time2 = await step1(time1)
const time3 = await step2(time2)
const result = await step3(time3)
console.log(`result is ${result}`)
console.timeEnd("doIt")
}
doIt();
7. Event Loop(事件循环)


- 堆,栈、队列
堆(Heap)是一种数据结构,是利用完全二叉树维护的一组数据。
栈(Stack)是只能表尾进行插入或删除操作的线性表,它按照后进先出的原则存储数据
队列(Queue)是只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作的线性表。故队列又称为先进先出
setTimeout(function () {
console.log('1')
});
new Promise(function (resolve) {
console.log('2'); // 同步任务进入主线程
resolve();
}).then(function () {
console.log('3')
});
console.log('4'); // 同步任务进入主线程
// 2,4,3,1
8. 斐波那契数列
// 闭包法
function fib() {
var arr = [0, 1]
return f = function (n) {
var res = arr[n]
if (typeof res !== 'number') {
arr[n] = f(n - 1) + f(n - 2)
res = arr[n]
}
return res
}
}
let p = fib()
p(10)
console.log(p(10)) //55
9. 数组去重
// 方法一:
function uniq(array){
var temp = []; //一个新的临时数组
for(var i = 0; i < array.length; i++){
if(temp.indexOf(array[i]) == -1){
temp.push(array[i]);
}
}
return temp;
}
var aa = [1,2,2,4,9,6,7,5,2,3,5,6,5];
console.log(uniq(aa));
// 方法二:
function uniq(array){
var temp = [];
var index = [];
var l = array.length;
for(var i = 0; i < l; i++) {
for(var j = i + 1; j < l; j++){
if (array[i] === array[j]){
i++;
j = i;
}
}
temp.push(array[i]);
index.push(i);
}
console.log(index);
return temp;
}
var aa = [1,2,2,3,5,3,6,5];
console.log(uniq(aa));
10. 事件冒泡,和事件捕获
事件冒泡和事件捕获分别是微软和网景公司提出的,正对页面中的事件流发生的顺序问题
如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="div">
点击了div
<p id="p">点击了p</p>
</div>
<script>
let div = document.getElementById('div')
let p = document.getElementById('p')
// div.onclick = function () {
// console.log('触发了div的事件')
// }
// p.onclick = function () {
// console.log('触发了p的事件')
// }
div.addEventListener('click', function () {
console.log('触发了div的事件')
},true)
p.addEventListener('click', function () {
console.log('触发了p的事件')
})
</script>
</body>
</html>
当一个div内部套了一个p标签,并且两者都绑定了各自的事件,当我们点击p标签的时候,会先触发哪一个事件,就是这个问题引发了。
事件冒泡: 事件冒泡 认为 事件会从底部开始向外扩散事件,也就是先出发内部p标签绑定是的事件函数,再触发外部的div事件函数。
事件捕获: 事件捕获 认为 外部的div会先捕获到用户点击的事件,所以div会先触发事件函数,在触发内部p标签的事件函数,与冒泡事件 恰好相反。
最后在W3C的联盟干预下,采用这种的方法,也就是在事件监听的函数addEventListener加入第三个参数,默认为false,代表采用冒泡事件处理机制,true则代表采用事件捕获的处理机制。