js知识点整理

js知识点(二)

记录一些js的小而碎的知识点,没有什么顺序,想到什么了就记下来了,帮助以后复习总结。

js、es、nodejs的区别:

-es:定义了各种语法,是语法和语义上的标准。 主要包括:语法、类型、语句、关键字、保留字、操作符、对象。
-js: 基于es标准实现的,但除了ECMAScript(语言基础,如:语法、数据类型结构以及一些内置对象)外,还包括web API。 如DOM、BOM等。
-nodejs: 除了包括es外,还包括操作系统、文件系统、网络系统、数据库等。基于V8引擎,运行在服务端的js。

js判断数组中是否有某个值?

​ 1、 array.indexOf 存在的话返回下标,否则返回-1
2、 array.includes 存在返回true,否则返回false
3、 array.find 返回数组中满足条件的第一个元素的值,没有返回undefined
4、 array.findIndex 返回数组中满足条件的第一个元素的下标,没有返回-1

数组扁平化?

将多级数组转化为一级数组。如: let ary = [1, [2, [3, [4, 5]]], 6]; // -> [1, 2, 3, 4, 5, 6]

​ 1、 调用es6中的flat方法: ary = arr.flat(Infinity);
2、 replace + split: ary = str.replace(/([|])/g, ‘’).split(‘,’)
3、 replace + JSON.parse:

str = str.replace(/(\[|\]))/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);

​ 4、 扩展运算符:

//只要有一个元素有数组,那么循环继续
while (ary.some(Array.isArray)) {
ary = [].concat(...ary);
}

高阶函数?

一个函数接收另一个函数作为参数或者返回值为一个函数,这种函数就是高阶函数。

1、map

接收两个参数,一个是函数,另一个是函数的this值
创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果
对原来的数组没有影响

let nums = [1, 2, 3];
let obj = {val: 5};
let newNums = nums.map(function(item,index,array) {
    return item + index + array[index] + this.val; 
    //对第一个元素,1 + 0 + 1 + 5 = 7
    //对第二个元素,2 + 1 + 2 + 5 = 10
    //对第三个元素,3 + 2 + 3 + 5 = 13
}, obj);
console.log(newNums);//[7, 10, 13]

2、reduce

参数: 接收两个参数,一个为回调函数,另一个为初始值。回调函数中三个默认参数,依次为积累值、当前值、整个数组。

let nums = [1, 2, 3];
// 多个数的加和
let newNums = nums.reduce(function(preSum,curVal,array) {
    return preSum + curVal; 
}, 0);
console.log(newNums);//6

不传默认值会自动以第一个元素为初始值,然后从第二个元素开始依次累计。

3、filter

参数: 一个函数参数。这个函数接受一个默认参数,就是当前元素。这个作为参数的函数返回值为一个布尔类型,决定元素是否保留。
filter方法返回值为一个新的数组,这个数组里面包含参数里面所有被保留的项。

let nums = [1, 2, 3];
// 保留奇数项
let oddNums = nums.filter(item => item % 2);
console.log(oddNums);

4、sort

参数: 一个用于比较的函数,它有两个默认参数,分别是代表比较的两个元素。

注意:就是比较函数不传的时候,是将数字转换为字符串,然后根据字母unicode值进行升序排序,也就是根据字符串的比较规则进行升序排序。

浅拷贝深拷贝?

浅拷贝:

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr);//[1, 2, 3]

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!这就是浅拷贝!
但是这又会带来一个潜在的问题:

1
2
3
4
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]

浅拷贝只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。需要使用深拷贝。
实现浅拷贝的几种方式:

1、手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}

2、Object.assign

Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

3、concat浅拷贝数组

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

4、slice浅拷贝

5、展开运算符

let arr = [1, 2, 3];
let newArr = [...arr];//跟arr.slice()是一样的效果

深拷贝:

1、JSON.parse(JSON.stringify());

但是这种方法会有一些问题:
1) 无法解决循环引用的问题。
2) 无法拷贝函数。
3) 无法拷贝特殊的对象,如RegExp,Date,Set,Map等,

typeof和instanceof

1、对于原始类型来说,除了null,typeof都能显示正确的类型。注意,null不是对象。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

2、对于引用类型(数组,对象)来说,除了函数显示function,其他都显示object。

3、typeof 一般只能返回如下几个结果:”number”、”string”、”boolean”、”object”、”function” 和 “undefined”

4、采用typeof判断对象数据类型是不合适的,采用instanceof会更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为true

5、手动实现instanceof? 核心: 原型链的向上查找

function myInstanceof(left, right) {
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null) return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeof(proto);
    }
}

Object.is 和 ===

Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。 源码如下:

function is(x, y) {
    if (x === y) {
        //运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
        return x !== 0 || y !== 0 || 1 / x === 1 / y;
    } else {
        //NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
        //两个都是NaN的时候返回true
        return x !== x && y !== y;
     }
}

== 和 ===

===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如’1’===1的结果是false,因为一边是string,另一边是number。
==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:

​ 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
判断的是否是null和undefined,是的话就返回true
判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较

console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true

对象转为原始类型是根据什么流程运行的?

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

1、如果Symbol.toPrimitive()方法,优先调用再返回
2、调用valueOf(),如果转换为原始类型,则返回
3、调用toString(),如果转换为原始类型,则返回
4、如果都没有返回原始类型,会报错

例子:

var obj = {
    value: 3,
    valueOf() {
        return 4;
    },
    toString() {
        return '5'
    },
    [Symbol.toPrimitive]() {
        return 6
    }
}
console.log(obj + 1); // 输出7

应用:如何让if(a == 1 && a == 2)条件成立?

1
2
3
4
5
6
7
8
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2); //true

闭包

什么是闭包:

闭包是指有权访问另外一个函数作用域中的变量的函数。
   闭包产生的本质就是,当前环境中存在指向父级作用域的引用。

闭包产生的原因:

1
2
3
4
5
6
7
8
9
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

并不是只有返回函数才算是产生了闭包,我们只需要让父级作用域的引用存在即可:

1
2
3
4
5
6
7
8
9
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();

让f1执行,给f3赋值后,等于说现在f3拥有了window、f1和f3本身这几个作用域的访问权限,还是自底向上查找,最近是在f1中找到了a,因此输出2。

在这里是外面的变量f3存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。

闭包的表现形式:

1、 返回一个函数。刚刚已经举例。
2、 作为函数参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();

3、在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

以下的闭包保存的仅仅是window和当前作用域:

1
2
3
4
5
6
7
8
9
// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)

// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})

4、IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window和当前函数的作用域,因此可以使用全局的变量。

1
2
3
4
5
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();

原型与原型链

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); // 'Zaxlct'

var person2 = new Person();
person2.sayName(); // 'Zaxlct'

console.log(person1.sayName == person2.sayName); //true

1、每个对象都有 proto 属性,但只有函数对象才有 prototype 属性
2、原型对象:就是proptype,即Person.proptype。
3、在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性(是一个指针)指向 prototype 属性所在的函数(Person)
即 Person.prototype.constructor == Person
4、实例的构造函数属性指向构造函数,即 person1.constructor == Person
5、person1 为什么有 constructor 属性?那是因为 person1 是 Person 的实例。
那 Person.prototype 为什么有 constructor 属性??同理, Person.prototype (你把它想象成 A) 也是Person 的实例。
也就是在 Person 创建的时候,创建了一个它的实例对象并赋值给它的 prototype
6、结论:原型对象(Person.prototype)是 构造函数(Person)的一个实例。
7、JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做proto 的内置属性,用于指向创建它的构造函数的原型对象。
即 person1.proto == Person.prototype