一、原型
1、原型链
1)原理
- 所有的函数,都有一个 prototype
- 所有的引用类型(数组、对象、函数)都有一个 _proto_ 属性(隐式原型属性
function Person(){}
let p1 = new Person()
let p2 = new Person()
let obj = {}
//写出 p1 p2 Person Function obj Object等的原型链
```
* p1: __proto__ : Person.prototype
* Person : __proto__: Function.prototype, prototype: Person.prototype
* Person.prototype : __proto__ : Object.prototype , constructor: Person
* Function: __proto__ : Function.prototype, prototype: Function.prototype
* Function.Prototype: __proto__ : Object.prototype , constructor: Function
* obj: __proto__ : Object.prototype
* Object: __proto__ : Function.prototype , prototype: Object.prototype
* Object.prototype: __proto__ : null , constructor : Object
* Object.__proto__ === Function.prototype
> Object是对象的构造函数,那么它也是一个函数,当然它的__proto__也是指向Function.prototype
2)实例
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}
var f = new F()
// 请问f有方法a 方法b吗
```
f.__proto__ === F.prototype
F.prototype.__proto__ === Object.prototype
f的__proto__指向F.prototype,F.prototype.__proto__指向Object.prototype,所以f 可以取到a方法, 由于f的原型链上没经过Function.prototype,所以取不到b方法。
由于构造函数F是由Function new出来的,所以F.__proto__指向Function.prototype,所以F函数可以取到b方法。
function A() {}
A.prototype.x = 10;
var a1 = new A();
A.prototype = { x: 20, y: 30 };
var a2 = new A();
console.log(a1.x);
console.log(a1.y);
console.log(a2.x);
console.log(a2.y);
2、ES5 和ES6 继承的区别
继承:一个对象直接使用另一个对象的属性和方法
在ES5的继承中,先创建子类的实例对象this,然后再将父类的方法添加到this上( Parent.apply(this) )。
ES6采用的是先创建父类的实例this(故要先调用 super( )方法),完后再用子类的构造函数修改this
3、内置NEW的实现原理
- 创建一个空的对象son{}
- son.proto === Father.prototype (空对象的原型链 指向构造函数的原型)
- 重新绑定this,使构造函数的this指向新对象 Father.call(this)
- 为新对象属性赋值son.name
- 返回this,此时新对象就拥有了构造函数的属性和方法
function _new(Func, ...args) {
//默认创建一个实例对象(而且是属于当前这个类的一个实例)
// let obj = {};
let obj = Object.create(Func.prototype);
//也会把类当做普通函数执行
//执行的时候要保证函数中的this指向创建的实例
let result = Func.call(obj, ...args);
//若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
if ((result !== null && typeof result === "object") || (typeof result === "function")) {
return result;
}
return obj;
}
1) 使用场景
实现链式调用
function Class1() {
console.log('初始化')
}
Class1.prototype.method = function(param) {
console.log(param)
return this
}
let cl = new Class1()
//由于new 在实例化的时候this会指向创建的对象, 所以this.method这个方法会在原型链中找到。
cl.method('第一次调用').method('第二次链式调用').method('第三次链式调用')
4、多继承
利用proxy拦截器
let foo = {
foo () {
console.log('foo')
}
}
let bar = {
bar () {
console.log('bar')
}
}
// 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo(); // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar(); // bar
// 黑科技开始
let sonOfFooBar = new Proxy({}, {
get (target, key) {
return target[key] || foo[key] || bar[key];
}
})
// 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有
sonOfFooBar.foo(); // foo 有foo方法,继承自对象foo
sonOfFooBar.bar(); // bar 也有bar方法,继承自对象bar
5、继承方式
参考:js 6种继承方式及优缺点 - 远方的少年🐬 - 博客园
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
eg:
function inheritPrototype(subType,superType){
var prototype=Object.create(superType.prototype)
prototype.constructor=subType
subType.prototype=prototype
}
function SuperType(name){
this.name=name
this.colors=["red","blue","green"]
}
SuperType.prototype.sayName=function(){
console.log(this.name)
}
function SubType(name,age){
SuperType.call(this,name)
this.age=age
}
inheritPrototype(SubType,SuperType)
SubType.prototype.sayAge=function(){
console.log(this.age)
}
6、ES6 class
super继承父类this
class Parent{
constructor(name){
this.name = name;
}
static sayHello(){
console.log('hello');
}
sayName(){
console.log('my name is ' + this.name);
return this.name;
}
}
class Child extends Parent{
constructor(name, age){
super(name);
this.age = age;
}
sayAge(){
console.log('my age is ' + this.age);
return this.age;
}
}
7、hasOwnProperty
避免原型链查找, 建议使用 hasOwnProperty 方法
console.log(instance1.hasOwnProperty('age'));//true
8、instanceof
左侧的__proto__ === 右侧的prototype**,则返回true
alert(instance instanceof Object);//true
9、isPrototypeOf
使用 isPrototypeOf() 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true
alert(Father.prototype.isPrototypeOf(instance));//true
10、in
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true
prop in object
如果你使用 delete 运算符删除了一个属性,则 in 运算符对所删除属性返回 false
var mycar = {make: "Honda", model: "Accord", year: 1998};
delete mycar.make;
"make" in mycar; // 返回false
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
delete trees[3];
3 in trees; // 返回false
如果你只是将一个属性的值赋值为undefined,而没有删除它,则 in 运算仍然会返回true
var mycar = {make: "Honda", model: "Accord", year: 1998};
mycar.make = undefined;
"make" in mycar; // 返回true
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
trees[3] = undefined;
3 in trees; // 返回true
11、Object.setPrototypeOf 方法的使用
语法: Object.setPrototypeOf(obj, prototype)
将一个指定的对象的原型设置为另一个对象或者null(既对象的[[Prototype]]内部属性).
function Person() {}
var s = new Person();
Object.setPrototypeOf(s, {name: 'yy'});
s.__proto__ //输出:{name: "yy"}
12、typeof
其实就是判断参数是什么类型
"number"、"string"、"boolean"、"object"、"function" 和 "undefined"
typeof 123; // number
typeof 'jartto'; // string
typeof !!’0’; // boolean
typeof new Function(); // function
typeof name; // undefined
let arr = [1,2,3];
let obj = {name: 'jartto'};
let obj1 = null;
typeof arr; // object
typeof obj; // object
typeof obj1; // object
如上所示,引用类型的数据,都返回了 object,我们无法做到精确判断。我们来总结一下:
1.对于基本类型,除 null 以外,均可以返回正确的结果。
2.对于引用类型,除 function 以外,一律返回 object 类型。
3.对于 null ,返回 object 类型。
4.对于 function 返回 function 类型。
小问题
解析:条件判断为假的情况有:0,false,'',null,undefined,未定义对象。函数声明写在运算符中,其为true,但放在运算符中的函数声明在执行阶段是找不到的。另外,对未声明的变量执行typeOf不会报错,会返回undefined
if (function f() {}) {
x += typeof f;
}
console.log(x)
二、异步
1、执行顺序小问题
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
var promiseA = new Promise(function(resolved) {
setTimeout(function() {
console.log('p1')
resolved('p1 resolved');
}, 0);
console.log('p0');
resolved('p0 resolved');
var promise2 = new Promise((resolved2) => {
console.log('p2');
resolved2('p2 resolved');
});
promise2.then((msg) => {
console.log(msg);
});
})
console.log('start');
promiseA.then((msg) => {
console.log(msg)
});
//p0 ,p2 ,start, p2 resolved, p0 resolved p1
2、Promise
- 无法取消 Promise
- 错误需要通过回调函数捕获
1) 立即输出 0,之后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5
const tasks = [];
for(var i =0; i < 5; i++) {
((j) => {
tasks.push(
new Promise(resolve => {
setTimeout(() => {
console.log(j)
resolve();
}, j * 1000)
})
)
})(i)
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i)
}, 1000)
})
2)红绿灯
var arr = ['red', 'yellow', 'green'];
var tasks = [];
var test = function() {
for(var i = 0; i < arr.length; i++ ) {
((j) => {
tasks.push(
new Promise(resolve => {
setTimeout(() => {
console.log(arr[j]);
resolve();
}, j * 1000)
})
)
}
)(i)
}
Promise.all(tasks).then(() => {
setTimeout(() => {
test();
}, 1000);
})
}
test();
3) promise实现一个sleep
function sleep(time) {
return new Promise(resolve => setTimeout(resolve,time))
}
!async function test() {
var t1 = new Date;
await sleep(3000);
var t2= new Date;
console.log(t2 - t1);
}()
4)执行顺序小问题
new Promise((resolve, reject ) => {
throw new Error()
}).then(() => {
console.log(1)
}, () => {
console.log(2)
}).catch(() => {
console.log(3)
}).then(() => {
console.log(4)
})
// 2 4
三、作用域及变量提升
1、实例
1) 通过var声明的函数的引用不会被后面的函数改变
var x=1,y=0,z=0;
var add=function(x){
return x = x+1;
}
y=add(x);
function add(x){
return x=x+3;
}
z = add(x);
console.log(x,y,z); // 1 2 2
2) 立即执行函数的作用域是window
var num = 1;
var myObject = {
num: 2,
add: function () {
this.num = 3;
(function () {
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function () {
console.log(this.num);
}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
3)变量提升问题
var x = 30;
function test() {
console.log(x);
var x = 10;
console.log(x);
x = 20;
function x() {}
console.log(x);
}
test();
function aa(val){ //函数参数的变量也会提升
console.log(val);//'函数传参'
var val='变量声明';
console.log(val);//'变量声明'
}
aa('函数传参');
var obj = {
a: 1
}
function test(obj) { //相当于生成一个新的值,指向了obj。
console.log('obj:', obj)
obj.a = 2
obj = {a: 4}
obj.b = 3
console.log('内部',obj)
}
console.log(test(obj))
console.log('外部:', obj)
/*解析
在这个立即执行函数表达式(IIFE)中包括两个赋值操作,其中a使用var关键字进行声明,因此其属于函数内部的局部变量(仅存在于函数中),相反,b被分配到全局命名空间。
另一个需要注意的是,这里没有在函数内部使用严格模式(use strict;)。如果启用了严格模式,代码会在输出 b 时报错Uncaught ReferenceError: b is not defined,需要记住的是,严格模式要求你显式的引用全局作用域。
*/
(function () {
var a = (b = 5);
})();
console.log(b);
console.log(a);
/*解析
立即调用的函数表达式(IIFE) 有一个 自己独立的 作用域,如果函数名称与内部变量名称冲突,就会永远执行函数本身;所以上面的结果输出是函数本身;
*/
var a = 1;
(function a () {
a = 2;
console.log(a);
})();
var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
this.num = num * 3
num++
return function (n) {
this.num += n
num++
console.log(num)
}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz;
}
function bar() {
fn();
}
foo();
bar(); // 2
四、类型判断
1、toString
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(newFunction()) ; // [object Function]
Object.prototype.toString.call(newDate()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(newRegExp()) ; // [object RegExp]
Object.prototype.toString.call(newError()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
2、instanceof
见1.8
3、typeof
见1.12
4、类型转换
- JS基本有5种简单数据类型:String,Number,Boolean,Null,undefined。
- 一种复杂的数据类型Object。(array, function, Data)
解析:
当 a 出现在 if 的条件中时,被转成布尔值,而 Boolean([0])为 true,所以就进行下一步判断 a == true,在进行比较时,[0]被转换成了 0,所以 0==true 为 false
数组从非 primitive 转为 primitive 的时候会先隐式调用 join 变成“0”,string 和 boolean 比较的时候,两个都先转为 number 类型再比较,最后就是 0==1 的比较了
var a = [0];
if (a) {
console.log(a == true);
} else {
console.log(a);
}
!![] //true 空数组转换为布尔值是 true,
!![0]//true 数组转换为布尔值是 true
[0] == true;//false 数组与布尔值比较时却变成了 false
Number([])//0
Number(false)//0
Number(['1'])//1
[] == 0 //true
![] == [] //true
[] == [] //false
![] == [] //true 。。。[] == ![]为什么是true。!的优先级比==要高,所以会先执行![]。也就是先把[]转为布尔类型再取反。[]转布尔值是true,为什么呢?因为在JavaScript里除了false自身以外只有5个假值,分别是“”,undefined, null, 0, NaN。除了这5个假值以外,其他所有值转布尔类型都是true。一切对象都是真值,包括new Boolean(false)。于是问题就成了刚才我们讨论的 [] == false了。故得到 [] == ![]为true
{} == {} //false
!{} == {} //false
null == 0 // false
//这时候两边的类型也不同,但是却没有做类型转换,why?因为这时候二者都已经是基本数据类型了,没有办法在进行转换了,所以二者的类型都不可能相同,结果自然为false
null == undefined // true
!![] == true //true
//这里并没有涉及 == 比较,只需要判断 [] 是true还是false即可
[] == false // true
五、模块化
- es6: import / export
- commonjs: require / module.exports / exports
- amd: require / defined
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
六、正则提取数字
let str = '2017-05-15T09:10:23 Europe/Paris';
let arr = str.match( /\d{1,}/g);
//match会返回一个数组,
// \d 查找数字
// {1,} 表示至少重复几次
// /g表示全局搜索
七、ES6
1、Set
const a = {}
const b = 1
const c = 'ScriptOJ'
const set1 = new Set([a, b, c])
const set2 = new Set([a, c, b])
console.log(isSameSet(set1, set2)) // => true
function isSameSet(set1, set2) {
return [...set1].every(item => set2.has(item)) && [...set2].every(item => set1.has(item))
}
2、解构赋值
1)交换两个整数
var a = 1,
b = 2,
//方法一:
var temp;
temp = a;
a = b;
b = temp;
//方法二:ES6 解构赋值
[a,b] = [b, a]
3、Proxy
1)应用场景:负索引
// 方法一:
const negativeArray = els =>
new Proxy(els, {
get: (target, propKey, receiver) =>
Reflect.get(
target,
+propKey < 0 ? String(target.length + +propKey) : propKey,
receiver
)
});
const unicorn = negativeArray(["京", "程", "一", "灯"]);
unicorn[-1];
// 方法二:
const proxyArray = arr => {
const length = arr.length;
return new Proxy(arr, {
get(target, key) {
key = +key;
while (key < 0) {
key += length;
}
return target[key];
}
})
};
var a = proxyArray([1, 2, 3, 4, 5, 6, 7, 8, 9]);
console.log(a[1]); // 2
console.log(a[-10]); // 9
八、数组
1)判断是否为数据
- Object.prototype.toString.call()
- Array.isArray()
- [].constructor
- [] instanceof Array
2) 多维数组展平
//多维数组展平
// 方法一
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
// 方法二
function flatten(arr) {
return arr.toString().split(',').map(function(item) {
return Number(item);
})
}
// 方法三
function flatten(arr) {
return arr.join(',').split(',').map(function(item) {
return parseInt(item);
})
}
// 方法四
function flatten(arr) {
var res = [];
arr.map(item => {
if(Array.isArray(item)) {
res = res.concat(flatten(item));
} else {
res.push(item);
}
});
return res;
}
// 方法五
[].concat(...[1, 2, 3, [4, 5]]); // [1, 2, 3, 4, 5]
function flatten(arr) {
while(arr.some(item=>Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
//方法六
arr.flat(Infinity)
3)类数组转换成数组
//方法一:
Array.prototype.slice.call(toArr)
//方法二: Array.from
Array.from(toArr)
//方法三:arguments => array [...arguments]
[...arguments]
4)数组去重
//方法一: include
var arr = [1,1,2,4,6,6]
var newArr = [];
function arrToWeight() {
arr.map(item => {
if(!newArr.includes(item)) {
newArr.push(item)
}
})
}
arrToWeight(arr);
//方法二: reduce
let names = [1, 2, 3, 4, 4, 3];
let nameNum = names.reduce((pre,cur)=>{
if(!pre.includes(cur)) {
return pre.concat(cur)
}else{
return pre;
}
},[])
console.log(nameNum); //[1, 2, 3, 4]
或者
let nameNum = names.reduce((pre,cur)=>{
if(!pre.includes(cur)) pre.push(cur)
return pre;
},[])
console.log(nameNum); //[1, 2, 3, 4]
//方法三: filter
let names = [1, 2, 3, 4, 4, 3];
function unique(arr) {
return arr.filter((item, index) => {
return arr.indexOf(item) === index
})
}
unique(names);
//方法四:ES6
let names = [1, 2, 3, 4, 4, 3];
[...new Set(names)]
//方法五:Object.keys()
//遍历,将数组的值添加到一个对象的属性名里,并给属性赋值,对象不能添加相同属性名,以这个为依据可以实现数组去重,然后用Object.keys(对象)返回这个对象可枚举属性组成的数组,这个数组就是去重后的数组。
let arr = ['1', '2', '3', 1,NaN,NaN,undefined,undefined,null,null, 'a', 'b', 'b'];
const unique = arr => {
var obj = {}
arr.forEach(value => {
obj[value] = 0;//这步新添加一个属性,并赋值,如果不赋值的话,属性会添加不上去
})
return Object.keys(obj);//`Object.keys(对象)`返回这个对象可枚举属性组成的数组,这个数组就是去重后的数组
}
console.log(unique(a));//["1", "2", "3", "NaN", "undefined", "null", "a", "b"]
5) 遍历
// orEach(), filter(), reduce(), every() 和some() 都会跳过空位。
// map()会跳过空位,但会保留这个值
// join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。
// ES6 中都会将空位当做undefined
let array = [,1,,2,,3];
array = array.map((i) => ++i)
九、reduce
/**
* @method reduce 计数:(arr.reduce(function(total, currentValue, currentIndex, arr), initialValue))
*/
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre;
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
//方法二:
let newObj = {}
names.map((item,i) => {
if (newObj[item]) {
newObj[item]++
}else {
newObj[item] = 1;
}
})
/**
* @method 数组去重
*/
let names = [1, 2, 3, 4, 4, 3];
let nameNum = names.reduce((pre,cur)=>{
if(!pre.includes(cur)) {
return pre.concat(cur)
}else{
return pre;
}
},[])
console.log(nameNum); //[1, 2, 3, 4]
/**
* @method [1, 2, 3, 4]>[2, 4, 6, 8]
*/
let names = [1, 2, 3, 4];
let nameNum = names.reduce((pre,cur)=>{
return pre.concat(cur*2)
},[])
console.log(nameNum);
/**
* @method 将二维数组转化为一维
*/
let arr = [[0, 1], [2, 3], [4, 5]]
let nameNum = arr.reduce((pre,cur)=>{
return pre.concat(cur) //concat 可以打散一层数组, 连接时不改变原数组
},[])
console.log(nameNum);
/**
* @method 将多维数组转化为一维
*/
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur)?newArr(cur): cur)
},[])
}
console.log(newArr(arr));
//方法二:
let pre = [];
let newArr = function(arr) {
arr.map((item,i) => {
if(Array.isArray(item)) {
newArr(item)
}else {
pre.push(item); //push 可以改变原数组
}
})
return pre;
}
/**
* @method 对象里的属性求和
*/
var result = [
{
subject: 'math',
score: 10
},
{
subject: 'chinese',
score: 20
},
{
subject: 'english',
score: 30
}
];
const sum = result.reduce((pre, cur) => {
return pre + cur.score
},0)
console.log(sum);
十、generator
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
//方法2 扩展运算符
[...iterTree(tree)]
十一、this
1)改变this指向方法
- call: fn.call(target, 1, 2)
- apply: fn.apply(target, [1, 2])
- bind: fn.bind(target)(1,2)
- 类实例化
- 箭头函数,指向当前定义环境
- return
-
- 在构造函数的时候,使用return进行返回一个Object的时候,当去new一个实例对象的时候,会将this指向改变为return的Object
- 运行时的调用者
小问题
- {} 不产生作用域,上下文this指向window
var a = 20;
function foo() {
var a = 1;
var obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a;
}
}
return obj.c;
}
console.log(foo()); // 40
console.log(window.foo()); // 40
十二、闭包
1) 用途
- 计时器
2) setTimeout
for(var i = 0; i < 5; i++) {
setTimeout((i) => {
console.log(i)
},i*1000)
}
==> 输出:1 2 3 4 5
//方法一:
for (var i = 1; i <= 5; i++) {
let _i = i;
setTimeout(function timer() {
console.log(_i);
}, i * 1000);
} //1 2 3 4 5
//方法二:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
} //1 2 3 4 5
//方法三:闭包(立即执行函数)
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
//方法四:闭包
for (var i = 1; i <= 5; i++) {
setTimeout((function (i) {
return function () {
console.log(i);
}
})(i), i * 1000);
}
//方法五:参数传递按照值传递
function fn(i) {
setTimeout(() => {
console.log(i);
}, 1000)
}
for (var i = 1; i <= 5; i++) {
fn(i)
}
十三、
十四、对象的拷贝
1、深拷贝的几种方法:
1)JSON.stringify 和 JSON.parse
缺点:JSON.parse(JSON.stringify(object)),缺点诸多(会忽略undefined、symbol、函数;不能解决循环引用;不能处理正则、new Date())
性能最快
2) Object.assign()拷贝
当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
3) 使用递归的方式实现深拷贝
function _deepClone(source) {
let target;
if (typeof source === 'object') {
target = Array.isArray(source) ? [] : {}
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] !== 'object') {
target[key] = source[key]
} else {
target[key] = _deepClone(source[key])
}
}
}
} else {
target = source
}
return target
}
4) 展开运算符(...)
5) structuredClone() 解决循环引用,不保留原型链
十五、事件
1、事件委托
如果用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;// target表示在事件冒泡中触发事件的源元素,在IE中是srcElement
if(target.nodeName.toLowerCase() == 'li'){
alert(123);
alert(target.innerHTML);
}
}
}
2、阻止事件冒泡 和 阻止事件默认行为
- event.stopPropagation()方法 这是阻止事件的冒泡方法,不让事件向documen上蔓延,但是默认事件任然会执行,当你掉用这个方法的时候,如果点击一个连接,这个连接仍然会被打开,
- event.preventDefault()方法 这是阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;
- return false ; 这个方法比较暴力,他会同事阻止事件冒泡也会阻止默认事件;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()和event.preventDefault()
3、stopPropagation与stopImmediatePropagation的区别
共同点:
都是阻止后续的侦听行为,即能阻挡掉事件流中事件的冒泡,简而言之就是让后面的侦听都不执行;
不同点:
是拥有事件监听函数的当前的节点是否执行该函数,stopPropagation()方法阻止事件对象移到到另一个节点上,但是允许当前节点的其他事件监听函数执行,而stopImmediatePropagation()方法不仅阻止事件从当前节点移动到另一个节点上,它还不允许当前节点的其他事件监听函数执行。
$(function(){
$(".testInput").keyup(function(e){
$(".show").html("<a href='http://www.candoudou.com' title='前端开发' target='_blank'>http://www.candoudou.com</a>");
//比较注释和不注释这一行的区别,stopImmediatePropagation可以阻止在这之后绑定的事件
//e.stopImmediatePropagation();
});
$(".testInput").keyup(function(e){
$(".show").html("<a href='http://www.rcttt.com' title='前端开发' target='_blank'>http://www.rcttt.com</a>");
});
});
十六、DOM怎样添加、移除、移动、复制、创建和查找节点
(1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入、克隆
appendChild()
removeChild()
replaceChild()
insertBefore()
cloneNode()
(3)查找
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的Name属性的值
getElementById() //通过元素Id,唯一性
十七、get、post
- GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符
- POST:一般用于修改服务器上的资源,对所发送的信息没有限制。
- GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值,
也就是说Get是通过地址栏来传值,而Post是通过提交表单来传值。
然而,在以下情况中,请使用 POST 请求:
无法使用缓存文件(更新服务器上的文件或数据库)
向服务器发送大量数据(POST 没有数据量限制)
发送包含未知字符的用户输入时,POST 比 GET 更稳定也更可靠
十八、AMD, CMD, CommonJS
1、CommonJS
CommonJS是服务器端模块的规范,Node.js采用了这个规范。
根据CommonJS规范,一个单独的文件就是一个模块。加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。
// foobar.js
//私有变量
var test = 123;
//公有方法
function foobar () {
this.foo = function () {
// do someing ...
}
this.bar = function () {
//do someing ...
}
}
//exports对象上的方法和变量是公有的
var foobar = new foobar();
exports.foobar = foobar;
//require方法默认读取js文件,所以可以省略js后缀
var test = require('./boobar').foobar;
test.bar();
CommonJS 加载模块是同步的,所以只有加载完成才能执行后面的操作。像Node.js主要用于服务器的编程,加载的模块文件一般都已经存在本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。但如果是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。所以就有了 AMD CMD 解决方案。
commonjs是运行时加载模块,ES6是在静态编译期间就确定模块的依赖
2、AMD、CMD
//AMD
define(['./a','./b'], function (a, b) {
//依赖一开始就写好
a.test();
b.test();
});
//CMD
define(function (requie, exports, module) {
//依赖可以就近书写
var a = require('./a');
a.test();
...
//软依赖
if (status) {
var b = requie('./b');
b.test();
}
});
*小问题
1、网络请求超时问题
参考:如何使 fetch() 请求超时_fetch 超时-优快云博客
a、背景
网络不可靠,因为 HTTP 请求或响应可能由于多种原因而失败:
- 用户离线
- DNS 查询失败
- 服务器没有响应
- 服务器响应但出现错误
- 和更多。
用户可以等待最多 8 秒来完成简单的请求。这就是为什么您需要对网络请求设置超时并在 8 秒后通知用户网络问题。
b、axios超时取消
const axios = require('axios')
// 1、获取CancelToken
var CancelToken = axios.CancelToken;
// 2、生成source
var source = CancelToken.source();
console.log(source.token)
axios.get('/user/12345', {//get请求在第二个参数
// 3、注入source.token
cancelToken: source.token
}).catch(function (thrown) {
console.log(thrown)
});
axios.post('/user/12345', {//post请求在第三个参数
name: 'new name'
}, {
cancelToken: source.token
}).catch(e=>{
console.log(e)
});
// 4、调用source.cancel("原因"),终止注入了source.token的请求
source.cancel('不想请求了');
方法二:
const CancelToken = axios.CancelToken;
let cancel;
let timer = setTimeout(() => {
cancel();
this.$message.error("连接超时,请检查网络!")
}, 10000);
this.axios({
method: "post",
url,
data,
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
})
.then((response) => {
clearTimeout(timer);
if (!response.data.hasError) {
this.$message.success("数据获取成功!");
const results = response.data.result;
} else {
this.$message.error("数据获取失败!");
}
})
.catch((error) => {
clearTimeout(timer);
if (error.response) {
this.$message.error("数据获取失败!");
}
});
axios 设置超时时间 timeout
全局设置网络超时
axios.defaults.timeout = 30000;
单独对某个请求设置网络超时
let timeout = parseInt(paramsTimeout);
this.$http.post(url, params, {timeout: timeout})
.then(res => {
console.log('response='+response);
})
.catch(reason => {
console.log('reason'+reason);
})
})
c、fetch超时
默认情况下,fetch()请求在浏览器指示的时间超时。在 Chrome 中,网络请求超时为 300 秒,而在 Firefox 中为 90 秒。
async function loadGames() {
const response = await fetch('/games');
// fetch() timeouts at 300 seconds in Chrome
const games = await response.json();
return games;
}
300 秒甚至 90 秒远远超过用户期望完成一个简单的网络请求。
fetch()API 本身不允许以编程方式取消请求。要在所需时间停止请求,您还需要一个中止控制器。
以下是创建具有可配置超时的请求fetchWithTimeout()的改进版本:fetch()
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
}
// 使用fetchWithTimeout()
async function loadGames() {
try {
const response = await fetchWithTimeout('/games', {
timeout: 6000
});
const games = await response.json();
return games;
} catch (error) {
// Timeouts if the request takes
// longer than 6 seconds
console.log(error.name === 'AbortError');
}
}
let controller = new AbortController();
let signal = controller.signal;
let timeoutPromise = (timeout) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Response("timeout", { status: 504, statusText: "timeout " }));
controller.abort();
}, timeout);
});
}
let requestPromise = (url) => {
return fetch(url, {
signal: signal
});
};
Promise.race([timeoutPromise(1000), requestPromise("https://www.baidu.com")])
.then(resp => {
console.log(resp);
})
.catch(error => {
console.log(error);
});
d、原生取消请求
切记:不可用abort方法来作为终止对服务器的请求操作,只有当做在前端页面立刻停止执行ajax成功后的方法,因为你执行abort方法后,ajax很可能已经对服务端发送了请求,只是还未返回回馈信息而已。
文档: https://www.toutiao.com/article/7072525164218483200/
//停止javascript的ajax请求
xmlHttp.open("POST", "Url", true);
xmlHttp.onreadystatechange = function () {
//得到响应之后的操作
}
xmlHttp.send();
//设置3秒钟后检查xmlHttp对象所发送的数据是否得到响应.
setTimeout("CheckRequest()", "3000");
function CheckRequest() {
//为4时代表请求完成了
if (xmlHttp.readyState != 4) {
alert('数据响应超时');
//关闭请求
xmlHttp.close();
}
} //根据响应状态的改变关闭
x.abort(); // 终止请求
2、白屏时间、首屏时间
1) 白屏时间
白屏时间是指浏览器开始显示内容的时间。因此,我们通常认为解析完 <head> 的时刻或开始渲染 <body> 标签就是页面白屏结束的时间。
可使用 Performance API 时
白屏时间 = firstPaint - performance.timing.navigationStart;
通常计算首屏的方法有
- 首屏模块标签标记法
- 统计首屏内加载最慢的图片的时间
- 自定义首屏内容计算法
2) 首屏时间
首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。
a、首屏模块标签标记法
首屏模块标签标记法,通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况。我们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:
此时首屏时间等于 firstScreen - performance.timing.navigationStart;
事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用 JavaScript 脚本来判断首屏页面内容加载情况。
b、统计首屏内图片完成加载的时间
通常我们首屏内容加载最慢的就是图片资源,因此我们会把首屏内加载最慢的图片的时间当做首屏的时间。
由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。因此我们在 DOM树 构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。
此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart;
c、自定义模块内容计算法
由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:
- 忽略图片等资源加载情况,只考虑页面主要 DOM
- 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容
3) 如何优化首屏加载,减少白屏时间
1)DNS 预解析
使用 meta 标签
<meta http-equiv="x-dns-prefetch-control" content="on" />
使用 link 标签
<link rel="dns-prefetch" href="https://www.baidu.com" />
2)使用 HTTP2
HTTP 相比于 HTTP1,解析速度更快;支持多路复用,多个请求可以共用一个 TCP 连接;提供了首部压缩功能;支持服务器推送,服务器可以在发送 HTML 页面时,主动推送其他资源,而不用等到浏览器解析到相应位置发请求再响应。
3)减少 HTTP 请求数量,减少 HTTP 请求大小
4)合并、压缩文件;按需加载代码,减少冗余代码
5)采用 svg 图片或字体图标
6)使用 Defer 加载 JS
尽量将 CSS 放文件头部,JS 文件放在底部,以免堵塞渲染。JS 如果放在头部,给 script 标签加上 defer 属性,异步下载,延迟执行。
7)服务端渲染
客户端渲染:获取 HTML 文件,根据需要下载 JavaScript 文件并运行,生成 DOM,然后再渲染。
服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。
优点:首屏渲染快,对搜索引擎优化(SEO)好。
缺点:配置麻烦,增加了服务器的计算压力。
8)静态资源使用 内容分发网络(CDN)
解决用户与服务器物理距离对响应时间的影响,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
9)资源缓存,不重复加载相同的资源
10)图片优化(雪碧图、图片懒加载、CSS 图片懒加载)
具体可参考性能优化篇
3、二进制转十进制
parseInt('11000000', 2) // 192
4、Fetch和Axios区别
Axios是对XMLHttpRequest的封装,
Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。
最大的不同点在于Fetch是浏览器原生支持,而Axios需要引入Axios库。
1) 请求方式
axios传一个对象,里面包含请求url和请求方法,参数
fetch传两个参数,第一个是请求url,第二个是请求的一些参数
1. Axios请求示例:
const options = {
url: "http://example.com/",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
data: {
a: 10,
b: 20,
},
};
axios(options).then((response) => {
console.log(response.status);
});
2. Fetch请求示例:
const url = "http://example.com/";
const options = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
a: 10,
b: 20,
}),
};
fetch(url, options).then((response) => {
console.log(response.status);
});
2) 响应超时
- axios请求可以直接设置timeout属性来控制响应超时
- fetch请求需要使用AbortController属性,使用起来没有axios方便
axios({
method: "post",
url: "http://example.com/",
timeout: 4000, // 请求4秒无响应则会超时
data: {
firstName: "David",
lastName: "Pollock",
},
})
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));
const controller = new AbortController();
const options = {
method: "POST",
signal: controller.signal,
body: JSON.stringify({
firstName: "David",
lastName: "Pollock",
}),
};
const promise = fetch("http://example.com/", options);
// 如果4秒钟没有响应则超时
const timeoutId = setTimeout(() => controller.abort(), 4000);
promise
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));
3) 对数据的转化
Axios还有非常好的一点就是会自动对数据进行转化,而Fetch则不同,它需要使用者进行手动转化。
// axios
axios.get("http://example.com/").then(
(response) => {
console.log(response.data);
},
(error) => {
console.log(error);
}
);
// fetch
fetch("http://example.com/")
.then((response) => response.json()) // 需要对响应数据进行转换
.then((data) => {
console.log(data);
})
.catch((error) => console.error(error));
4)HTTP拦截器
- axios提供了请求和相应拦截器
- Fetch没有拦截器功能,但是要实现该功能并不难,直接重写全局Fetch方法就可以办到。
fetch = ((originalFetch) => {
return (...arguments) => {
const result = originalFetch.apply(this, arguments);
return result.then(console.log("请求已发送"));
};
})(fetch);
fetch("http://example.com/")
.then((response) => response.json())
.then((data) => {
console.log(data);
});
5)同时请求
//Axios:
axios
.all([
axios.get("https://api.github.com/users/iliakan"),
axios.get("https://api.github.com/users/taylorotwell"),
])
.then(
axios.spread((obj1, obj2) => {
...
})
);
//Fetch:
Promise.all([
fetch("https://api.github.com/users/iliakan"),
fetch("https://api.github.com/users/taylorotwell"),
])
.then(async ([res1, res2]) => {
const a = await res1.json();
const b = await res2.json();
})
.catch((error) => {
console.log(error);
});
6)浏览器原生支持
Fetch唯一碾压Axios的一点就是现代浏览器的原生支持。
在当前网页打开Chrome的控制台使用Fetch几乎不需要什么配置就可以直接进行请求。
个人总结:
1. 如果在项目中使用的话还是Axios使用起来方便
2. 如果是在浏览器控制台测试,或者想快速请求的话,可以使用Fetch,因为它不需要导入,是浏览器支持的。
小问题
1. require和import的定义和基本用法
require是Node.js中CommonJS模块系统的语法,用于在服务器端和浏览器端加载模块。它是在运行时加载模块,返回模块导出的内容,通常与module.exports
一起使用。
import是ES6(ECMAScript 2015)及以后版本的语法,用于在编译时加载模块。它是在代码执行前加载模块,支持静态分析和解构赋值,通常与export
一起使用。
a. require和import的主要区别
- 加载方式:
-
- require是运行时加载,可以在代码的任何地方使用。
- import是编译时加载,必须放在文件开头。
- 规范不同:
-
- require遵循CommonJS规范。
- import遵循ES6模块规范(ESM)。
- 静态与动态:
-
- require是动态的,无法进行静态分析。
- import是静态的,支持编译时的静态分析。
- 解构赋值:
-
- require导入整个模块。
- import支持解构赋值,可以选择性地导入模块中的特定部分。
b. require和import的使用场景
- 在Node.js环境中,通常使用require来加载模块,因为它符合CommonJS规范。
- 在浏览器端或者需要现代JavaScript特性的环境中,通常使用import来加载模块,因为它符合ES6规范。
- Vue.js等现代前端框架中,经常使用import来实现路由懒加载,以提高页面加载速度和用户体验。
c. require和import的未来发展趋势
随着JavaScript生态的发展,import的使用越来越广泛,成为模块化的标准做法。虽然目前一些老旧的浏览器不支持import,但通过工具如Babel可以将ES6代码转码为ES5,使得import可以在更广泛的环境中使用。