JS基础


title: JS基础

date: 2019-08-19 13:39:26

tags: javascript

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • JS # 一级分类
  • JS基础 # 二级分类

JS 对象

对象,就是一种无序的数据集合,由若干个“键值对”(key-value)构成。

{key: value}是 JS 对象字面量写法

数据类型

number, boolean, string, undefined, null, object, symbol

原始类型,又叫基本类型,值类型,number, string, boolean,undefined,null。指保存在栈内存里的简单数据段

引用类型,对象 object。又可分为狭义的对象,数组,函数,正则。指保存在堆内存中的对象

其中 js 的 number 类型是浮点类型,不是整型.并且浮点类型基于 IEEE 754 标准实现,在使用中会遇到某些 Bug。

NaN 属于 number 类型,NaN表示Not a Number,并且 NaN 不等于自身.

undefined: 表示未定义或不存在.由于没有定义,所以目前没有值

null: 表示无值.即此处的值就是”无”的状态.

typeof

typeof 对于基本类型,除了 null 都可以显示正确的类型

typeof undefined 输出 undefined

typeof []// ‘object’

typeof {} // ‘object’

typeof function(){} //‘function’(这是具体的判断)

typeof null 输出 object

PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。

运算符

  1. && (And)
  2. || (or)
  3. ! (not)
1
2
3
a = 1 && 3; //3
a = 1 || 3; //1
!2 * 0; //0

instanceof运算符,判断是否为某一种的实例.

=====,前者先转化类型,后进行判断,后者比较类型和值

三目运算符

condition ? true case : false case

四则运算符

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
加法拼接两个字符串
console.log("2"+"4") //"24"
加法将其中不是字符串的一方转化为字符串进行拼接
console.log(2+"4") //"24"
只有一个字符串参数时将其转化成数字
console.log(+"4") //4
console.log(+new Date()) //1573972012164
字符串加对象会把对象变成[object Object]
console.log('aa:'+{a:1}) //aa:[object Object]

在参数有对象的情况下调用其valueof或toString,其中valueof优先级更高
console.log(2+new Date()) //"2Sun Nov 17 2019 14:21:50 GMT+0800 (香港标准时间)"

valueof()返回适合该对象类型的原始值。

toString()将该对象的原始值以字符串形式返回。

这两个方法一般是交由 js 去隐式调用。

在数值运算中,优先调用 valueof().

在字符串运算中,优先调用 toString().

自增运算符: a 或 a

1
2
(a = 1), (b = a++); //a=2,b=1
(a = 1), (b = ++a); //a=2,b=2

无论是先加,还是后加,a 的值计算后一定会加 1;

1
2
(a = 3), (b = 4);
a++ + b; //相当于(a++)+b => 3 + 4 = 7

逗号运算符

取后一个表达式的值.也就是说,前一个会被覆盖.

1
var d = ((a = 1), (b = 2)); // 2

boolean 值

false: undefined null 0 NaN 空字符串

除此之外,其他所有值都转化为 true,包括所有对象.

==比较相等性

相等操作符会先转换操作数(通常称为强制转型),然后比较它们的相等性。

在转换不同的数据类型时,相等操作符遵循下列基本规则:

  1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值;
  2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值;
  3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较;
  4. 如果有一个操作数是 NaN,无论另一个操作数是什么,相等操作符都返回 false;
  5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象,则相等操作符返回 true;
  6. 在比较相等性之前,不能将 null 和 undefined 转成其他值。
  7. null 和 undefined 是相等的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[] == [] //false
{} == {} //false
声明引用类型的时候,变量名保存在 js 的栈内存里面,而对应的值保存在堆内存里面
而这个变量在栈内存中实际保存的是:这个值在堆内存中的地址,也就是指针.
如果
a = {}
b = {}
虽然 a 和 b 都保存了一个 Object,但这是两个独立的 Object,它们的地址是不同的.
再结合前面的第5条规则:如果两个对象指向同一个对象,相等操作符返回 true
所以 {} == {} 的结果是 false,同样的, [] == [] 的结果也是 false

[] == ![] //true
按优先级,先判断![],是false. =>[] == false.[]是对象,调用[].toString返回"",false转化成0. =>"" == 0.Number("")是0,那么结果就是true.
{} == !{} //false
与上面类似,不过Number({})返回 NaN
"hello" == true //先转化,后比较.true转化成数字是1,"hello"转化成数字是NaN,二者不相等.
"hello" == false //false转化为数字是0,NaN不等于0.二者也不相等.

JSON 格式(javascript object notation)

  1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
  2. 简单类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和 null(不能使用 NaN, Infinity, -Infinity 和 undefined)。
  3. 字符串必须使用双引号表示,不能使用单引号。
  4. 对象的键名必须放在双引号里面。
  5. 数组或对象最后一个成员的后面,不能加逗号。

JSON.stringify

用于将一个值转为字符串。该字符串符合 JSON 格式,并且可以被 JSON.parse 方法还原。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JSON.stringify("abc"); // ""abc""
JSON.stringify(1); // "1"
JSON.stringify(false); // "false"
JSON.stringify([]); // "[]"
JSON.stringify({}); // "{}"

JSON.stringify([1, "false", false]);
// '[1,"false",false]'

JSON.stringify({ name: "张三" });
// '{"name":"张三"}'
var arr = [undefined, function () {}];
JSON.stringify(arr); // "[null,null]"

JSON.stringify(/foo/); // "{}"
//正则对象会被转成空对象。

上面代码将各种类型的值,转成 JSON 字符串。

需要注意的是,对于原始类型的字符串,转换结果会带双引号。

如果原始对象中,有一个成员的值是undefined、函数或 XML 对象,这个成员会被过滤。

如果数组的成员是undefined、函数或 XML 对象,则这些值被转成null

正则对象会被转成空对象。

JSON.parse()

用于将 JSON 字符串转化成对象。

1
2
3
4
5
6
7
8
JSON.parse("{}"); // {}
JSON.parse("true"); // true
JSON.parse('"foo"'); // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse("null"); // null

var o = JSON.parse('{"name": "张三"}');
o.name; // 张三

JSON.parse 只能解析字符串,下面这种情况会报错.

1
2
3
4
5
6
var str = { name: "张三" };
var o = JSON.parse(str);
o.name; // 报错
解决方法: var str = '{"name": "张三"}';
或者;
var o = JSON.parse("str");

JavaScript 对象的字面量写法只是长的像 JSON 格式数据,二者属于不同的范畴,JavaScript 对象中很多类型(函数、正则、Date) JSON 格式的规范并不支持,JavaScript 对象的字面量写法更宽松。

写 JavaScript 的基本规范

1.不要在同一行声明多个变量。

2.请使用 ===/!==来比较 true/false 或者数值

3.使用对象字面量替代 new Array 这种形式

4.不要使用全局函数。

5.Switch 语句必须带有 default 分支

6.函数不应该有时候有返回值,有时候没有返回值。

7.For 循环必须使用大括号

8.If 语句必须使用大括号

9.for-in 循环中的变量 应该使用 var 关键字明确限定作用域,从而避免作用域污染。

JS 函数

函数会声明前置

1
2
3
4
foo(); //hello
function foo() {
return "hello";
}

函数表达式不会

1
2
3
4
fn()//报错
var fn() = fnction(){
return "hello"
}
1
2
3
4
5
6
7
8
var str = "一号";
test();
function test() {
console.log(str);
var str = "二号";
console.log(str);
}
console.log(str);

输出

1
2
3
4
5
//考察变量声明和作用域
undefined; //因为打印str时,首先从str所在的作用域找变量str,
//因为有声明前置,var str,但是未赋值,是undefined.所以不再从外部找str
二号;
一号; //函数test内部的str=二号只在函数内部,最后一个str的值是全局下的即"一号"

js 对象查看所有属性

1
2
var obj = { p: 1 };
Object.keys(obj);

删除命令

delete object.p

循环判断

1
2
3
4
5
6
7
8
9
10
switch(判断语句){
case 条件:
输出;
break;
case 条件:
输出;
break;
default:
输出
}

while 循环

1
2
3
4
5
while (expression) {
statement;
}
//如果expression为true,执行,
//循环继续执行直到判断不成立。

do-while 循环

1
2
3
4
do {
statement;
} while (expression);
//先执行,后判断

for 循环

1
2
3
for (var i = 0; i < 5; i++) {
console.log(i);
}

执行步骤如下:

初始化语句: var i=0,只执行第一次

判断语句: i<5

条件改变语句: i++

内部执行: console.log(i)

先执行初始化语句,然后判断条件是否成立,成立就执行内部语句,最后改变条件.重复进行判断.

for-in 循环

1
2
3
4
var arr = [1, 2, 3];
for (var key in arr) {
console.log(arr[key]);
}

break退出本次循环

continue跳过本次循环执行下次循环

引用类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//a,b存放在栈内存中
var a = 1;
var b = 2;
//obj是对象,存放在堆内存中,obj只是一个指针,一个地址
var obj = {
name: "Tom",
sex: "male",
age: 15,
friend: {
name: "Jerry",
age: 10,
},
};
var newobj = {}; //空对象也是引用类型,只不过暂时还没有东西

//基本类型
b = a;
console.log(b); //1,把a赋值给b,b在栈内存中也被改变为1
//引用类型
var obj2 = obj;
//二者都是引用类型,都是把地址指向同一个地方,改变内存中的值,另一个也会改变
obj2.name = "Ann";
console.log(obj.name); //'Ann'
1
2
3
4
var obj3 = { name: "hello" };
var obj4 = { name: "hello" };
obj3 === obj4; //false,
//虽然二者内容看似相同,但是确是不同的内存中,两个地址
1
2
3
4
5
6
function sum() {
console.log("sum..");
}
var sum2 = sum;
sum2(); //'sum..',
//函数同样是引用类型,当赋值给另一个函数时,二者也就指向同一地址

函数的参数传递

1
2
3
4
5
6
7
function inc(n) {
n++;
}
var a = 10;
inc(a);
console.log(a); //10
//inc(a)把a传递进去,先赋值后相加,a=10,inc(a) = 11,a并没有变化还是10
1
2
3
4
5
6
7
function incObj(obj) {
obj.n++;
}
var o = { n: 10 };
incObj(o);
console.log(o); //{n:11}
//incObj(o),o.n++,那么{n:11},堆内存中的数值已经改变,输出o时,o也就变成了{n:11}

深拷贝

1
2
3
4
5
6
7
8
9
var obj = {
name: "hunger",
age: 3,
friends: ["aa", "bb", "cc"],
};

var obj2 = JSON.parse(JSON.stringify(obj));
obj.age = 4;
console.log(obj2.age);

缺点: JSON 不支持函数、引用、undefined、RegExp、Date……

jQuery.extend()

jQuery.extend( [deep], target, object1 [, objectN ] )

其中 deep 为 Boolean 类型,如果是true,则进行深拷贝。

1
2
3
4
5
6
7
8
9
10
11
var target = { a: 1, b: 1 };
var copy1 = { a: 2, b: 2, c: { ca: 21, cb: 22, cc: 23 } };
var copy2 = { c: { ca: 31, cb: 32, cd: 34 } };
var result = $.extend(true, target, copy1, copy2); // 进行深拷贝
console.log(target); // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 23, cd: 34}}

var target = { a: 1, b: 1 };
var copy1 = { a: 2, b: 2, c: { ca: 21, cb: 22, cc: 23 } };
var copy2 = { c: { ca: 31, cb: 32, cd: 34 } };
var result = $.extend(target, copy1, copy2); // 不进行深拷贝
console.log(target); // {a: 1, b: 1, c: {ca: 31, cb: 32, cd:34}}

递归深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone(object) {
var object2;
if (!(object instanceof Object)) {
return object;
} else if (object instanceof Array) {
object2 = [];
} else if (object instanceof Object) {
object2 = {};
}
//你也可以把 Array Function Object 都当做 Object 来看待
for (let key in object) {
object2[key] = clone(object[key]);
}
return object2;
}

函数

函数返回值 return

通过 return 返回结果,调用 return 后,函数立即中断并返回结果,即使后面还有语句也不再执行.

立即执行函数

将整个函数表达式包含在括号内,最后添加一个括号表示执行.

1
2
3
(function () {
console.log("hello");
})();

命名冲突

当在同一个作用域内定义了名字相同的变量和方法的话,无论其顺序如何,变量的赋值会覆盖方法的赋值.

1
2
3
var fn = 3;
function fn() {}
console.log(fn); //3

当函数执行有命名冲突时,函数执行时载入顺序是变量,函数,参数

1
2
3
4
5
6
7
8
function fn(fn) {
console.log(fn);

var fn = 3;
console.log(fn);
}

fn(10); //10 3

函数作用域(scope)

作用域(scope)指的是变量存在的范围。

在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。

词法作用域

创建当前函数所在的作用域.

递归

一个函数可以指向并调用自身。调用自身的函数我们称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件(避免无限循环或者无限递归)。

作用域链

  1. 函数在执行的过程中,先从自己内部找变量
  2. 如果找不到,再从创建当前函数所在的作用域去找, 以此往上
  3. 注意找的是变量的当前的状态

闭包(closure)

它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。

一般情况下使用闭包主要是为了

  1. 封装数据
  2. 暂存数据

面试官想听的版本:

什么是闭包?

由于在 JS 中,变量的作用域属于函数作用域,在函数执行后作用域就会被清理、内存也随之回收,但是由于闭包是建立在一个函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会随之销毁,这时的子函数——也就是闭包,便拥有了访问上级作用域中的变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。

闭包解决了什么问题?

由于闭包可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。以平时使用的 Ajax 成功回调为例,这里其实就是个闭包,由于上述的特性,回调就拥有了整个上级作用域的访问和操作能力,提高了极大的便利。开发者不用去写钩子函数来操作上级函数作用域内部的变量了。

闭包的应用?

闭包随处可见,一个 Ajax 请求的成功回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影。

1
2
3
4
5
6
7
8
9
10
11
12
13
function car() {
var speed = 0;
function fn() {
speed++;
console.log(speed);
}
return fn;
}

var speedUp = car();
speedUp(); //1
speedUp(); //2
//第二次执行speedup时,在fn内部,speed此时因为没有释放,还是上次的值1,也就相当于var speed = 1;不用再去上一级作用域找speed的值,可以直接相加输出

如果没有这个闭包,函数执行后,里面 speed 变量就会被清理掉。但我们声明了 fn 这个函数,并把它返回出来赋值给新的变量 speedup。因为 speedup 是全局变量,是一直存在的,故这个 fn 函数就一直存在,speed 变量也不会被清理

1
2
3
4
5
6
7
8
9
10
//演化版
var speedup = (function (speed) {
return function () {
speed++;
console.log(speed);
};
})(3);
speedup(); //4
speedup(); //5
speedup(); //6

经典范例

如下代码输出多少?如果想输出 3,那如何改造代码?

1
2
3
4
5
6
7
var fnArr = [];
for (var i = 0; i < 10; i++) {
fnArr[i] = function () {
return i;
};
}
console.log(fnArr[3]()); // 10

我的理解: fnArr[i]声明的匿名函数的作用域是全局作用域,先遍历,并未执行,生成闭包(只是 for 循环,并没有执行函数,后面没有小括号),遍历结束开始执行,fnArr 结果是 10 个匿名函数f(),全局作用域下 0-9 的fnArr[i]已经变成 10 了

原理解析:for 循环每次执行,都把 function(){ return i} 这个函数赋值给 fnArr[i],但这个函数不执行。

因为 fnArr[3] =function(){ return i};故当我们调用 fnArr3 时,相当于 function(){ return i};这个函数立刻执行,这时 for 循环已经完成,i 已经变成了 10。故输出 10

如果要输出 3,需要如下改造

1
2
3
4
5
6
7
8
9
var fnArr = [];
for (var i = 0; i < 10; i++) {
(function (i) {
fnArr[i] = function () {
return i;
};
})(i); //这样保证遍历i时,函数会立即执行(生成10个闭包,暂存数据)
}
console.log(fnArr[3]()); // 3

我的理解: 从 0 开始遍历,(function(0){ fnArr[0] = function(){return i}})(0),

(function(1){ fnArr[1] = function(){return i}})(1)

(function(2){ fnArr[2] = function(){return i}})(2),

(function(3){ fnArr[3] = function(){return i}})(3),

传入 3,最内部的 function 找 i 的变量,一直找到上一层,返回 3.

1
2
3
4
5
6
7
8
9
var fnArr = [];
for (var i = 0; i < 10; i++) {
fnArr[i] = (function (j) {
return function () {
return j;
};
})(i); //生成10个闭包,暂存数据,j换成i也是一样
}
console.log(fnArr[3]()); // 3

如下代码输出什么?

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
//输出5个5

如何连续输出 0,1,2,3,4

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i);
}
//0,1,2,3,4
1
2
3
4
5
6
7
8
//第二种方法
//把var换成let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
//0,1,2,3,4
1
2
3
4
5
6
7
8
9
10
11
12
var an = function (n) {
for (var i = 0; i < 2; i++) {
n++;
console.log(n); //2,3. 遍历两次,内部就打印两次
console.log(i); //0,1. 遍历两次,内部就打印两次
}
console.log(n); //3. 打印最终结果
console.log(i); //2. 打印最终结果.
//如果换成let,只有这里报错,因为i只在for循环体内被定义
};

var g = an(1);

函数柯里化

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数.

写一个函数 sum,实现如下调用

1
2
3
4
5
6
7
8
9
console.log(sum(1)(2)); //3
console.log(sum(5)(-1)); //4

//解答
function sum(a) {
return function (b) {
return a + b;
};
}

字符串

1
2
str.charAt(0); //获取第一个字符
str.charCodeAt(0); //获取字符对应的ASC码

字符串截取

1
2
3
4
var str = "hello";
str.substr(1, 3); //第一个开始,第二个长度
str.substring(1, 3); //第一个开始,第二个结束
str.slice(1, 3); //同上,允许负参

查找

1
2
3
4
str.search("he"); //返回下标
str.indexOf("he"); //同上
str.replace("he", "you"); //替换
str.match("he"); //匹配

大小写

1
2
str.toUpperCase();
str.toLowerCase(); //本身不变化

去除首尾空格

1
str.trim(); //返回新数组,不影响原数组

split()方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为子字符串,以确定每个拆分的位置。

找到分隔符后,将其从字符串中删除,并将子字符串的数组返回。如果没有找到或者省略了分隔符,则该数组包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将 str 转换为字符数组。如果分隔符出现在字符串的开始或结尾,或两者都分开,分别以空字符串开头,结尾或两者开始和结束。因此,如果字符串仅由一个分隔符实例组成,则该数组由两个空字符串组成。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var str = "The quick brown fox jumps over the lazy dog.";

var words = str.split(" ");
console.log(words[3]);
// expected output: "fox"

var chars = str.split("");
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

当字符串为空时,split()返回一个包含一个空字符串的数组,而不是一个空数组,如果字符串和分隔符都是空字符串,则返回一个空数组。

1
2
3
var str = "";
var strEm = str.split();
console.log(strEm); //返回[""]

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
arr.push(newelement) //末尾新增一个
arr.pop() //末尾踢出一个字
arr.shift() //开头拿出一个
arr.unshift() //开头新增一个
arr.splice(index,替换几个,替换内容) //原数组变
//返回由删除元素组成的新数组
arr.slice(start,end) //原数组不变
arr.join("-") //将数组变成字符串并用-连接
arr.reverse() //数组反转
arr.sort(v1-v2) //没有参数按顺序排列,但是会出现10,11,7,8,9排列,
//有参数设置比较函数,函数结果v1-v2如果大于0,两两替换位置。否则,不变
a.concat(array) //拼接,返回数组a和array的集合。原数组不变

Array.isArray(obj) //判断传入的obj是不是数组
a.indexof(2) //判断数组a中是否含有2,返回其索引

a.forEach(fn(element,index,arr){})
//forEach里是一个回调函数,有三个参数,分别是当前元素,索引值,整个数组
a.map(fn(element){})
//与forEach类似,返回新数组,原数组不变,是被一个函数处理后的数组.
//而forEach会修改原数组

a.every(fn(element,index,arr){})
//every是所有函数返回true才会返回true,遇到false终止,并返回false
a.some(fn(element,index,arr){})
//some是存在一个返回true时就终止返回true

a.filter(fn(element){})
//返回新数组,回调函数用于逻辑判断,为true则把当前元素加到返回的数组中
//为false则不加

a.reduce(fn(v1,v2),value)
//将数组元素合成一个值,从索引值最小开始。value表示初始值

a.abs(x) //返回x的绝对值

Array.from(new Set(a)) //数组去重

数组扁平化

1
2
3
4
5
6
7
8
9
10
const flattenDeep = (arr) =>
Array.isArray(arr)
? arr.reduce((a, b) => [...a, ...flattenDeep(b)], [])
: [arr];

flattenDeep([1, [[2], [3, [4]], 5]])[
//flatMap方法,目前该函数在浏览器中还不支持
(1, [2], 3)
].flatMap((v) => v + 1);
// -> [2, 3, 4]

数组去重

双重循环去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++>{
if(arr[i]===arr[j]){
//如果数组第一个等于第二个
arr.splice(j,1)
//就干掉第j个
j--
//因为数组arr被干掉一个,长度-1,j就相应的-1
}
})
}
return arr
}

new Set 去重

1
2
3
4
5
6
7
8
9
function unique(arr) {
return Array.from(new Set(arr));
}
//简化版
function unique(arr) {
return [...new Set(arr)];
}
//精简版
var unique = (arr) => [...new Set(arr)];

如何将浮点数点左边的数每三位添加一个逗号

如 12000000.11 转化为『12,000,000.11』?

1
2
3
4
5
6
7
8
function commafy(num) {
return (
num &&
num.toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($1, $2) {
return $2 + ",";
})
);
}

如何实现数组的随机排序?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
方法一:
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort1(arr){
for(var i = 0,i < arr.length; i++ ){
var rand = parseInt(Math.random()*len);
var temp = arr[rand];
arr[rand] = arr[i];
arr[i] = temp;
}
return arr;
}
console.log(randSort1(arr));

方法二:
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort2(arr){
var mixedArray = [];
while(arr.length > 0){
var randomIndex = parseInt(Math.random()*arr.length);
mixedArray.push(arr[randomIndex]);
arr.splice(randomIndex, 1);
}
return mixedArray;
}
console.log(randSort2(arr));

方法三:
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.sort(function(){
return Math.random() - 0.5;
})
console.log(arr);

Math

四舍五入

1
2
Math.round(0.5); //1
Math.round(-1.5); //-1

绝对值

1
Math.abs(-1); //1

最大值

Math.max() 函数返回一组数中的最大值。

参数是value1, value2, ...

是一组数值

1
2
3
Math.max(10, 20,30);
Math.max.call(null,10, 20,30);
Math.max.apply(null,[10, 20,30]);
1
2
3
4
floor返回小于参数值的最大整数.向下取整
Math.floor(-3.2) //-4
ceil返回大于参数值的最小参数,向上取整
Math.ceil(-3.2) //-3

parseInt(值,进制)

1
2
3
parseInt(string, radix);
//将一个字符串 string 转换为 radix 进制的整数,
// radix 为介于2-36之间的数。

parseInt()可以将字符串转为数字,也可以将数字取整。向下取整。

如果第一个字符是数字会解析直到遇到非数字结束,如果第一个字符不是数字或者符号(如:+、-)就返回 NaN。

带自动净化功能;带自动截断小数功能,且取整,不四舍五入。

radix: 基数,也就是进制.介于 2-36 之间,超出都是返回 NaN.radix 不写默认是十进制.如果传入字符串是 0x 开头,默认是 16 进制.如果是 0 开头,默认 8 进制.任何数的 0 进制都是它本身.高于进制位能表示的最大值数返回 NaN.比如说 2 的一进制返回 NaN,因为一进制只能用 1 表示,2 作为一进制来表示明显不对.

几次方

1
2
Math.pow(x, y); //返回 x 的 y 次幂的值。
Math.pow(2, 2); //4

平方根

1
2
Math.sqrt(4); // 2
Math.sqrt(-4); //NaN

random

1
2
Math.random();
//返回0-1之间的伪随机数.0≦x<1

Date

1
2
3
Date.now(); //获取1970年至今的毫秒数
Date.parse(); //解析日期字符串,返回距离1970年的毫秒数
Date.parse("2011-01-11"); //13234436600000
1
2
3
4
5
6
7
new Date(); //使用Date构造函数创建一个Date实例,
//不传时间就返回当前时间的字符串

var str = "2019-01-11";
new Date(str);
//Fri Jan 11 2019 08:00:00 GMT+0800 (香港标准时间)
//获取的是东八区8点的时间,因为格林尼治时间伦敦是0点

get 方法

1
2
3
4
5
6
7
8
getDate(); //日
getDay(); //星期几,周日是0,周一是1
getFullyear(); //返回四位的年份
getMouth(); //返回月份,一月是0,腊月是11
getHours(); //小时(0-23)
getMinutes(); // 返回分钟
getSenconds(); // 秒
getMilliseconds(); // 毫秒(0-999)

set 方法类似于 get 方法,是设置时间

时间格式化过滤器

Vue 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{{date | dateFormat }}

<script>
filters: {
//时间格式化
dateFormat: function(date) {
//date是传入的时间字符串
let t = new Date(date);
return (
t.getFullYear() +
"-" +
(t.getMonth() + 1) +
"-" +
t.getDate() +
" " +
t.getHours() +
":" +
t.getMinutes() +
":" +
t.getSeconds()
);
}
},
</script>

异步和回调

setTimeout 和 setInterval

区别: setInterval 指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行.

清除定时器: clearTimeout()和 clearInterval()

运行机制

setTimeout 和 setInterval 的运行机制是,将指定的代码移出本次执行,等到下一轮 Event Loop,再检查是否到了指定时间.如果到了,就执行对应代码,不到,就等到下一轮重新判断.这也就意味着,setTimeout 指定的代码,必须等到本次执行的所有代码都执行完,才会执行.

单线程模型

异步的任务放在任务队列(callback queue)中,setTimeout 也是.必须等放在栈(stack)中的同步代码执行完,才会执行任务队列中的.

event loop(事件循环)

经典案例:

1
2
3
4
5
for(var i=0; i<5; i++){
setTimeout(fucntion timer(){
comsole.log(i)
},1000)
}

我的理解:

开始循环遍历.(同步代码)

i=0.创建一个 console.log(i).放到队列里,并不执行.

i=1.创建一个 console.log(i).放到队列里,并不执行,

直到 i=5.依次创建了 5 个 console.log(i)

因为 setTimeout 是异步,所以必须等同步执行完才开始执行异步.

同步代码 i 循环到 5,输出,有 5 个 console.log(i)在队列里嗷嗷待哺

,把 i=5 赋值进去.得到 5 个 5.

函数节流

在时间内,如果执行,就重新开始

原理:当达到了一定的时间间隔就会执行一次;可以理解为是缩减执行频率

函数节流会用在比 input, keyup 更频繁触发的事件中,如 resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。

常用场景:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  2. 手机号、邮箱验证输入检测
  3. 窗口大小 Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

简单版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(fn, ms) {
let timer; //命名一个定时器
return function () {
if (timer) {
//如果有定时器就退出
return;
}
timer = setTimeout(fn, ms);
};
}
function fn() {
console.log("hello");
}
var fn2 = throttle(fn, 1000);

时间戳版本:

1
2
3
4
5
6
7
8
9
10
const throttle = (fn, wait) => {
let last = 0;
return () => {
const current_time = +new Date();
if (current_time - last > wait) {
fn.apply(this, arguments);
last = +new Date();
}
};
};

函数防抖

原理:将若干函数调用合成为一次,并在给定时间过去之后,或者连续事件完全触发完成之后,调用一次(仅仅只会调用一次)

适合场景:

  1. 滚动加载,加载更多或滚到底部监听
  2. 谷歌搜索框,搜索联想功能
  3. 高频点击提交,表单重复提交
1
2
3
4
5
6
7
8
9
10
11
12
function debounce(fn, ms) {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimerout(fn, ms);
};
}
//实际上上面这就可以了
function vlog() {
console.log(1);
}
window.onscroll = debounce(vlog, 500);

带有立即执行选项的防抖函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 这个是用来获取当前时间戳的
function now() {
return +new Date();
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce(func, wait = 50, immediate = true) {
let timer, context, args;

// 延迟执行函数
const later = () =>
setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null;
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args);
context = args = null;
}
}, wait);

// 这里返回的函数是每次实际调用的函数
return function (...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later();
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params);
} else {
context = this;
args = params;
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer);
timer = later();
}
};
}

对于按钮防点击来说的实现:

如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。

对于延时执行函数来说的实现:

清除定时器 ID,如果是延迟调用就调用函数

节流和防抖的区别

他们的区别在于防抖只会在连续的事件周期结束时执行一次,而节流会在事件周期内按间隔时间有规律的执行多次。

我的理解:

函数节流:

只要又执行函数,就退出.必须等上一次定时器任务执行完.这一段时间只执行一次.

函数防抖:

只要又执行函数,就清除定时器,重新设置定时器.只要重复就重新开始,最后肯定有一次.

DOM 元素创建和选取

DOM 是文档对象模型,是 html 的编程接口

readyState 加载状态

1
2
3
4
5
6
7
8
9
10
11
document.location;
//location属性返回一个只读对象,提供当前文档的url信息
document.location.href; //获取url
document.location.protocol; //获取"http:"
document.location.hostname; // "www.example.com"
document.location.port; // "4097"
document.location.pathname; // "/path/a.html"
document.location.assign("http://www.Google.com");
//跳转到指定网址
document.location.reload(true); //优先从服务器重新加载
document.location.reload(false); //优先从本地缓存加载
1
2
3
document.open(); //新建文档可以使用write写入
document.close(); //关闭文档
document.write(); //写入文档,会清除之前的文档流

Element表示 html 元素

  1. element 的属性:
  2. nodeName: 元素标签名
  3. nodeType:元素类型
  4. className:类名
  5. id:元素 id
  6. children:子元素列表
  7. childNodes: 子元素列表(NodeList)
  8. firstChild: 第一个子元素
  9. lastChild:最后一个
  10. nextSibling: 下一个兄弟元素
  11. previousSibling: 上一个兄弟元素
  12. parentNode.parentElement: 父元素

查询元素

1
2
3
4
5
6
7
8
9
10
11
12
13
document.getElementById("target");

document.getElementsByClassName("box");
//上述为ES3写法
//下面是ES5写法
document.querySelector(".box");
document.querySelector("#tatget");
//多个元素只会选择第一个

//选择多个
document.querySelectorAll("div");
//浏览器控制台测试时可以用$替代document.querySelector
//用$$替代document.querySelectorAll

创建元素

1
2
3
4
5
6
7
document.createElement("div");
//生成文本节点
document.createTextNode("Hello");
//生成DocumentFragment对象
document.createDocumentFragment();
//存在于内存中的DOM片段,不属于当前文档。
//对它的任何改动,都不会引发网页的重新渲染

修改元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在元素末尾添加元素;
appendChild();
newDiv.appendChild(newContent);

在元素之前插入元素;
insertBefore();

替换元素;
replaceChild(newElement, oldElement);

删除元素;
parentNode.removeChild(childNode);

clone元素;
node.cloneNode(true);
//方法有一个布尔值,传入true时会深拷贝,
//复制元素及其子元素,false只会复制元素本身

DOM 元素属性

1
2
3
4
5
6
7
8
9
10
11
12
获取属性
node.getAttribute("id")

生成新的属性对象节点
attribute = document.createAttribute(name)

一般用 setAttribute()设置元素属性
var node = document.getElementById("id")
node.setAttribute("id", "newVal")

删除属性节点
node.removeAttribute("id")
  • innerText可写属性,返回元素内包含的文本内容,在多层次的时候按照元素由浅到深的顺序拼接内容
1
2
3
4
5
6
<div>
<p>
123
<span>456</span>
</p>
</div>

外层 div 的 innerText 返回内容是  "123456"

  • innerHTML返回元素 html 结构,在写入的时候也会自动构建 DOM

样式

修改样式

1
document.querySelector("p").style.color = "red";

获取样式 getComputedStyle

1
2
3
4
var node = document.querySelector("p");
var color = window.getComputedStyle(node).color;
console.log(color);
//获取计算后的样式,只读。全局属性

class 操作的增删改查

1
2
3
4
5
6
var nodeBox = document.querySelector(".box");
console.log(nodeBox.classList);
nodeBox.classList.add("active"); //新增class
nodeBox.classList.remove("active"); //删除
nodeBox.classList.toggle("active"); //新增删除切换
node.classList.contains("active"); //判断是否拥有class

页面宽高

1
2
3
4
5
6
7
element.clientHeight; //包括padding,相当于IE盒模型,content-box
element.offsetHeight; //包括border,相当于标准盒模型,border-box
element.scrollHeight; //滚动区域的总高度(元素的起始位置的底边到停止位置的顶边),包括元素的padding,但不包括元素的border和margin
element.scrollTop; //滚动的高度
element.offsetTop; //返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。
window.innerHeight; //视口的高度,不包括标题栏等,包括水平滚动条
window.outerHeight; //整个浏览器窗口的高度,包括标题栏等所有浏览器内容

如何判断一个元素是否出现在窗口视野中

滚动的距离 + 窗口的高度 = 目标元素到顶部的距离

1
2
3
`window.scrollTop + window.innerHeight = element.offsetTop`;
//方法二
$("element").offset().top <= $(window).height() + $(window).scrollTop();

如何判断页面滚动到底部

1
2
3
4
5
//窗口滚动的距离 + 窗口的高度 = 整个页面的高度
$(window).scrollTop() + $(window).height() = $('body').height()
//方法二
//元素滚动的距离 + 元素的高度 = 元素的滚动区域的高度
element.scrollTop + element.clientHeight = element.scrollHeight

事件

DOM 事件流: 捕获阶段,目标阶段,冒泡阶段

事件绑定

1
2
3
4
5
6
7
8
<input id="btnClick" type="button" value="Click here">

<script type="text/javascript">
var btnClick = document.getElementById("btnClick")
btnClick.onclick = function showMessage(){
alert(this.id)
}
</script>

点击事件是异步的.

会存在覆盖

DOM2 事件处理(升级版)

  • addEventListener //绑定事件
  • removeEventListener //解绑事件

所有 DOM 节点都包含这两个方法,且都接受三个函数:

  1. 事件类型
  2. 事件处理方法
  3. 布尔参数,true表示在捕获阶段调用,false表示在事件冒泡阶段处理,默认是冒泡阶段
1
2
3
4
5
6
7
btnClick.addEventListener(
"click",
function () {
alert(this.id);
},
false
);

不存在覆盖,可以写多个方法

属性方法

1
2
3
preventDefault(); //取消默认事件行为
stopPropagation(); //取消事件进一步捕获或冒泡
target(); //事件的目标元素

事件代理

事件委托(代理)就是利用冒泡的原理,把事件加到父元素或祖先元素上,触发执行效果。

使用事件代理来实现它,监听的元素应该是这些元素的父元素,当我点击父元素内的元素时,父元素都会得到响应,并分发相应的事件。  e.target就是点击的元素。

1
2
3
4
5
6
7
$(".container").onclick = function (e) {
console.log(this);
console.log(e.target);
if (e.target.classList.contains("box")) {
console.log(e.target.innerText);
}
};

常见事件及自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
鼠标事件;
click;
dblclick; //双击左键
mouseover; //鼠标放上
mouseout; //鼠标移出
mouseenter; //类似over,区别不计算子元素
mouseleave; //类似out,区别不计算子元素
mousedown; //鼠标按下
mouseup; //鼠标松开

触摸事件;
touchstart;
touchend;
touchmove;

键盘事件;
keyup; //按键后松开触发
keydown; //按键即会触发
keypress; //按下松开

页面相关事件;
onload; //加载完成时触发
onmove; //浏览器窗口被移动
scroll; //滚动条滚动
resize; //窗口大小变化

表单相关;
focus; //获取焦点
blur; //失去焦点
change; //失去焦点且内容发生改变
reset; //reset属性被激活
submit; //提交,一般是表格提交
input; //在input元素内容修改后立即触发
onload; //页面所有资源加载完成
DOMContentLoaded; //dom结构解析完成

编辑事件;
beforecopy; //复制之前
beforecut; //剪切之前
beforepaste; //粘贴之前
beforeeditfocus; //将要进去编辑状态
contextmenu; //按右键出现菜单或者键盘触发页面菜单
losecapture; //失去鼠标移动所形成的选择焦点
select; //被选择时

拖动事件;
drag; //某个对象被拖动
dragdrop; //外部对象被拖到当前窗口
dragend; //拖动结束
dragenter; //被拖动对象进入其容器范围

js 拖动 div

  1. 使用mouse实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<head>
<meta charset="UTF-8" />
<title> mouse实现div拖动 </title>
<style type="text/css">
#div {
position: absolute;
left: 100px;
top: 100px;
width: 200px;
height: 200px;
background-color: #f60;
}
</style>
</head>
<body>
<div id="drag"></div>
<script type="text/javascript">
var drag = document.getElementById("drag");
var dragFlag = false;
var x,y;

drag.onmousedown = function (e) {
e = e || window.event;
x = e.clientX - drag.offsetLeft;
y = e.clientY - drag.offsetTop;
dragFlag = true;
};

document.onmousemove = function (e) {
if (dragFlag) {
e = e || window.event;
drag.style.left = e.clientX - x + "px";
drag.style.top = e.clientY - y + "px";
}
};

document.onmouseup = function (e) {
dragFlag = false;
};

</script>
</body>
  1. drag 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<head>
<style>
.divblok {
background-color:#ccc;
width:30px;
height:30px;
position:absolute;
left:20px;
top:20px;
}
</style>
</head>
<body>
<div id="dragdiv" draggable="true" class="divblok">我要移动 </div>
<script>
var dragdiv = document.querySelector('#dragdiv');
var x, y; //记录到点击时鼠标到移动框左边和上边的距离

dragdiv.addEventListener('dragstart', function (e) {
e.dataTransfer.effectAllowed = "move"; //移动效果
e.dataTransfer.setData("text", ''); //附加数据, 没有这一项,firefox中无法移动
x = e.offsetX || e.layerX;
y = e.offsetY || e.layerY;
return true;
}, false);

document.addEventListener('dragover', function (e) {//取消冒泡 ,不取消则不能触发 drop事件
e.preventDefault()|| e.stopPropagation();
}, false);

document.addEventListener('drop', function (e) {
dragdiv.style.left = (e.pageX - x) + 'px';
dragdiv.style.top = (e.pageY - y) + 'px';
e.preventDefault() || e.stopPropagation(); //不取消,firefox中会触发网页跳转到查找setData中的内容
}, false);
</script>
</body>

自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var EventCenter = {
on: function (type, handler) {
document.addEventListener(type, handler);
},
fire: function (type, data) {
return document.dispatchEvent(
new CustomEvent(type, {
detail: data,
})
);
},
};
EventCenter.on("hello", function (e) {
console.log(e.detail);
});
EventCenter.fire("hello", "你好");
//一个监听函数,一个处理函数

JS 动画

requestAnimationFrame

可以在一定时间内自动执行

1
2
3
4
5
6
7
8
9
function move() {
if (moveOffset < offsetX) {
ball.style.left = parseInt(getComputedStyle(ball).left) + step + "px";
moveOffset += step;
requestAnimationFrame(move);
}
}

move();

BOM

bom 指浏览器对象模型,核心是 window 对象。是浏览器的实例

window.innerHeight属性,window.innerWidth属性.这两个属性返回网页的 CSS 布局占据的浏览器窗口的高度和宽度,单位为像素。很显然,当用户放大网页的时候(比如将网页从 100%的大小放大为 200%),这两个属性会变小。

注意,这两个属性值包括滚动条的高度和宽度。

  1. scrollX:滚动条横向偏移
  2. scrollY:滚动条纵向偏移

这两个值随着滚动位置变化而变化

window.frames返回一个类似数组的对象,成员为页面内的所有框架,包括 frame 元素和 iframe 元素。

需要注意的是,window.frames 的每个成员对应的是框架内的窗口(即框架的 window 对象),获取每个框架的 DOM 树,需要使用window.frames[0].document

1
2
var iframe = window.getElementsByTagName("iframe")[0];
var iframe_title = iframe.contentWindow.title;

上面代码用于获取框架页面的标题。

iframe 元素遵守同源政策,只有当父页面与框架页面来自同一个域名,两者之间才可以用脚本通信,否则只有使用 window.postMessage 方法。

在 iframe 框架内部,使用 window.parent 指向父页面。

指向一个包含浏览器相关信息的对象.

window.getComputedStyle

getComputedStyle 是一个可以获取当前元素所有最终使用的 CSS 属性值

1
var style = window.getComputedStyle("元素", "伪类")

URL 的编码/解码方法

JavaScript 提供四个 URL 的编码/解码方法。

  1. decodeURI()
  2. decodeURIComponent()
  3. encodeURI()
  4. encodeURIComponent()

区别

encodeURI 方法不会对下列字符编码

1
2
3
1. ASCII字母
2. 数字
3. ~!@#$&*()=:/,;?+'

encodeURIComponent 方法不会对下列字符编码

1
2
3
1. ASCII字母
2. 数字
3. ~!*()'

alert(),prompt(),confirm()

alert()、prompt()、confirm()都是浏览器用来与用户互动的方法。它们会弹出不同的对话框,要求用户做出回应。

需要注意的是,alert()、prompt()、confirm()这三个方法弹出的对话框,都是浏览器统一规定的式样,是无法定制的。