记录自己JavaScript基础概念完整学习的过程
本文是在学习廖雪峰老师的JavaScript教程时 所归纳的笔记
廖雪峰老师教程在这里
注意点:
当下问数据类型就要考虑es6了, es6之前的是 null undefined number Boolean object string, es6的是symbol
number
不分整数和浮点数 统一为Number
number可以直接做四则运算 + - * / ; % 是求余运算
比较数据时需要注意的地方:
1. == 如果是不相同数据类型做比较,它会自动转换数据类型再比较(隐式转换)
2. === 不会转换比较对象, 它比较会判断类型 不同类型的数据 无论值如何 一律返回false 相同的话才会根据值返回
3. NaN与所有制都不相等 包括自己
NaN === NaN // false
唯一能判断NaN的方法是 isNaN() 函数
4. 浮点数不要直接比较 最好是转化为整数再比,因为js计算浮点数存在精度丢失问题
null和undefined
一个是 空 一个是 未定义
JavaScript的设计者希望用null
表示一个空的值,而undefined
表示值未定义。事实证明,这并没有什么卵用,区分两者的意 义不大。大多数情况下,我们都应该用null
。undefined
仅仅在判断函数参数是否传递的情况下有用
object
普通对象的键值都是字符串类型, es6 中的Map对象的键值可以是任何类型
变量
这里只说三个注意点:
1. 变量命名是大小写英文 数字 $和_ 这四类的组合,且首位不能为数字
2. 变量名不能是javascript关键字
3. 变量生命符现在有 let var const 三种,可点进去查看详细解释
string 字符串
基础字符串不多做赘述。
转义字符串:
比如 一个字符串中即包含' 也包含" // i'm "ok"
想要输出这种,可以用转义字符串改写为 // 'i\'m \"ok \" ' 即可
即 转义字符 \ 要写在被转义字符的前面
常用的转义字符有: \n 换行 \t 制表符 \本身也要转义 如果要输出\ 写法就是 \\
涉及到多行字符串,如果想要其效果输出为多行 即有换行效果
则\n 就比较费事了, so es6新增了标准多行字符串的表示方法
即 ` ... ` 反引号包裹
` 这是一首
简单的
小情歌
唱着我们心头的
曲折 `
模板字符串
对于字符串及变量拼接操作,
常规的是这样的
var a = 'hello'; var b = a + 'world'; // b hello world
如果有很多变量需要连接,+ 就会比较麻烦
es6中新增了一种 模板字符串
表示方法如下
var a = 'hello'; var b = `${a} world`; b // hello world
用反引号包裹 反引号中${}内的部分既是对于变量的值
var str = 'hey bro';
常见的有:
str.length // 取字符串长度
str[0] // h 取指定位置的值 取法类似数组那样 通过对应索引取值
tips:
字符串是不可变的,如果根据某个索引对字符串指定位置赋值,不会报错,但是也不会有任何效果
var s = 'nb'; s[0] = 's'; console.log(s) // nb
js为操作字符串提供的一系列方法:
这些方法不会改变原有字符串内容,统一返回一个新的字符串。
1. toUpperCase() 把字符串全部变为大写
2. toLowerCase() 把字符串全部变为小写
3. indexOf() 会搜索指定字符串出现的位置 并返回该位置的下标值 若找不到 返回-1
4. substring() 返回指定索引区间的子串
let s = 'hey bro'; s.substring(0,5); // hey br s.substring(1); // ey bro
数组 array
严格来说,array是object的特殊类型
对array的一系列操作方法如下:
1. arr.length // 获取array的长度
tips: 如果直接给arr的length赋一个新值,会导致array的大小发生变化
var arr = [1,2,3]; arr // length 3 arr.length = 6; arr // [1,2,3,undefined,undefined,undefined] arr.length = 2; arr // [1,2]
2. arr可以通过索引改变对应值,该修改会直接改变arr本身
tips: 通过索引赋值时,如果索引值超过了arr原有范围,同样会引起arr大小变化
3. indexOf() 和string的indexOf()用法 返回均相同
4. slice() 截取arr的部分元素, 返回一个新的数组 // 不会对arr产生变化
tips: slice()的起止参数为开始索引,结束索引, 返回的新数组包括开始索引,不包括结束索引
如果不给任何参数,会从头到尾截取,相当于复制一个新的数组
5. push() 向arr尾部添加若干元素
6. pop() 把最后一个元素删掉
7. unshift() 向头部添加若干元素
8. shift() 将头部第一个元素删掉
9. sort() 将arr排序, 自定义顺序会在后面讲到。 // 改变原数组
10. reverse() 将arr顺序完全颠倒 即反转 // 改变原数组
11. splice() 修改arr指定区域的元素 包括直接删除 替换
tips: splice() 返回的值是指定的索引对应的元素区域 是一个新数组
原数组会依据调用方式发生对应变化
12. concat() 拼接数组 // 返回新数组
tips: concat()可以接受任意个元素和数组,并最终拼接成一个新数组
13. join() 数组转字符串 会依据join(x)中添加的x拼接起来
tips: 如果array的元素不是字符串, 将自动转换为字符串后再连接
14.
对象 object
检测对象 object 是否具有某一项属性,可以用in操作符:
let obj = { name: '97', weight: 84, age: 24 }; 'name' in obj; // true 'sex' in obj; // false
tips:
用 in 来判断属性是否存在,有一个坑, 因为这个属性不一定是obj的,可能是obj继承得到的
比如
'toString' in obj; // true
因为toString是定义在object中的,而所有对象原型链的顶部都是object,则obj也拥有toString属性
为了规避in的这种情况,可以使用hasOwnProperty()方法
该方法判断 obj自身是否拥有某属性
obj.hasOwnProperty('name'); // true obj.hasOwnProperty('toString'); // false
基本的判断类型不再赘述
下面说一些记录点:
1. js里 在做数据类型转换时, 只把 null undefined 0 NaN和'' 视为false,其它一概为true // if(ah)
2. for()循环的三个条件均可省略, 即for(;;), 如果没有退出循环的条件,必须用break跳出,否则就是死循环
3. for...in 对对象进行操作时 建议使用hasOwnProperty()过滤掉对象继承的属性
var o = { name: 'Jack', age: 20, city: 'Beijing' }; for (var key in o) { if (o.hasOwnProperty(key)) { console.log(key); // 'name', 'age', 'city' } }
4. for...in 对array循环时 得到的索引不是number,而是string
因为基本对象object的键值限制为String类型,为了可以用其它类型做键值
es6引入了新的数据规范Map
Map
map是一组键值对的结构,具有极快的查找速度
拟一个情况,要根据同学的名字查找对应的成绩,
如果用array实现,则需要一个数组记录名称,一个数组记录成绩
var names = ['Michael', 'Bob', 'Tracy']; var scores = [95, 75, 85];
来自阮一峰:Map和Set讲义
感觉不能拿这种结构来举例子,因为它们是没有强映射关系的,应该用常规对象 如下
let obj = {
{ name: 'a', score: 75 },
{ name: 'b', score: 66 },
{ name: 'c', score: 90 },
};
随便给定一个值 66 想要拿到name的值 一定会用循环, 而且 数据体积越大,查找越慢,耗时越长
如果用Map实现,只需要构造成key-value结构,无论数据体积有多大,查找速度都不会慢。
var m = new Map([['a', 75],['b', 66],['c', 90]]); m.get('a'); // 直接输出75
tips:
初始化Map需要一个二维数组,或者直接初始化空Map,再对其操作。 Map自带的操作方法如下:
var m = new Map(); // 空map m.set('a', 66); // 添加key-value值操作 m.set('b', 55); // m.has('b'); // 是否存在key b: true m.get('b'); // 55 m.delete('b'); // 删除key b m.get('b'); // undefined 由于key不能重复,故重复对相同的key设值,后面的值会覆盖前面的值
Set
set只是key的集合,并非key-value这种形式
初始化Set需要一个数组,或者空
Set中不会包含重复值,有自动过滤去重操作
Set有两个方法:
add(key) 添加key值 重复添加不会报错 但是无效
delete(key) 删除指定key
iterable
对常规array遍历可以用下标循环,原来的map和set无法使用下标。 为了统一 集合类型,就有了iterable类型
Array Map Set 都属于iterable类型
具有iterable类型的集合 可以通过 for...of来遍历 // 属于es6新特性
var a = ['A', 'B', 'C']; var s = new Set(['A', 'B', 'C']); var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); for (var x of a) { // 遍历Array console.log(x); } for (var x of s) { // 遍历Set console.log(x); } for (var x of m) { // 遍历Map console.log(x[0] + '=' + x[1]); }
for...in 和 for...of 的区别
for...in 遍历的实际上是对象的属性名, array实际上也是对象,so它的每个元素的索引被当做一个属性。
手动给array对象添加属性后, for ... in 的效果如下:
var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var i in a) { consolg.log(i); // '0' '1' '2' 'name' } for...in把name也包裹进去了, length没有 for ... of 只循环集合本身的元素: var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var x of a) { console.log(x); // 'A', 'B', 'C' }
更好的方案是使用iterable内置的forEach()方法,它接收一个函数,每次迭代就自动回调该函数。 以Array为例
var a = ['A', 'B', 'C']; a.forEach(function(ele,index,array) { // ele: 指向当前元素 // index: 指向当前索引 // array: 指向array对象本身 即a })
tips: Set的forEach 因为没有索引 前两个参数都是元素本身
Map的forEach 依次为 value key Map本身
arguments
只在函数内部起作用,并且永远指向当前函数调用者传入的所有参数,可以简单的理解为,传入函数的所有参数的参数集合
tips: 即使函数未定义形参,只要有参数传入,通过arguments就可拿到传入的所有值
function abs() { if(arguments.length === 0) { return 0 } let x = arguments[0]; return x >= 0 ? x : -x; } abs(); // 0 abs(10); // 10 abs(-9); // 9
arguments最常用于判断传入参数的个数。
// foo(a[, b], c) // 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null: function foo(a, b, c) { if (arguments.length === 2) { // 实际拿到的参数是a和b,c为undefined c = b; // 把b赋给c b = null; // b变为默认值 } // ... }
为了规范化不设定参数数量的函数
es6标准引入了rest参数
即
function foo(a,b, ...rest) { console.log('a', a); console.log('b', b); console.log('rest', rest); }
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是
undefined
)
变量作用域
JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量
变量提升
js的函数定义 有一个特点, 它会先扫描整个函数体的语句, 把所有申明的变量提升到函数顶部
'use strict': function foo() { var x = 'Hello' + y; // line 1 console.log(x); // line 2 var y = 'Bob'; // line 3 } foo(); 虽然是strict模式,但是 line 1并未报错 原因就在于变量y在随后声明了,但是line 2打印的y值依然是undefined
这是因为,
虽然js引擎会自动提升变量声明,但是不会提升变量赋值,即赋值操作还是在line 3进行的
javascript只有一个全局作用域。任何变量(包括函数),如果没有在当前函数作用域下找到,就会继续往上查找,最后如果在全局作用域中也没有找到,就会报referenceError错误
全局变量会绑定到window上,不同的js文件如果定义了相同名字的顶层函数,或者使用了相同的全局变量,都会造成 命名冲突
减少冲突的一个方法是, 把某一个js文件下的所有变量和函数全部绑定到一个全局变量中:
// 唯一的全局变量myApp var myApp = {}; // 其他变量 myApp.other = 'aabb'; // 其他方法 myApp.foo = function () { return 'foo'; }
把自己的代码全部放入唯一的名字空间中,可大大减少全局变量冲突的可能。
像 jq yui underscore 等js库都是这么做的
为了解决js变量没有局部作用域的问题,es6引入了新的关键字let let 可代替var申明一个块级作用域变量:
'use strict'; function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用变量i } 'use strict'; function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } i += 1; // SyntaxError: }
es6之前 申明常量通常使用大写的变量来表示: ‘这是一个常量,不要修改它的值’;
es6 引入了const来定义常量
const 与 let 都具有块级作用域
const a = 123; a = 44; // 值为基本数据类型的常量是不可修改其值的 大部分浏览器会报错 少部分不报错 但是不会有效果 a // 123 const a = [1,2,3]; a[0] = 'c'; a // ['c',2,3]
传统赋值做法:
var array = ['hello', 'JavaScript', 'ES6']; var x = array[0]; var y = array[1]; var z = array[2];
es6之后 有了解构赋值,现在我们这么做
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
解构赋值的要点之一就是 嵌套层次和位置要保持一致
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']]; x; // 'hello' y; // 'JavaScript' z; // 'ES6'
也对对象进行解构赋值
let obj = { name: '', sex: '', age: '', address: { city: '', province: '' } } let {name,sex,age} = obj; // 写法一 对象的解构取值 如果取的值不存在 则会赋给其undefined let {id} = obj; // undefined let {address:{city, province}} = obj; // 对象的解构同样可以多层 只要嵌套解构对应即可 解构中的先后位置无硬性要求 let {school = false, sex} = obj; // 解决取值不存在的undefined问题 赋默认值
有些时候,如果变量已经被声明,再次赋值的时候,正确的写法也会报错
// var x,y; {x, y} = {name: 'sb', x: 100, y: 200} // 语法错误: Uncaught SyntaxError: Unexpected token = // 这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来: ({x, y} = { name: '小明', x: 100, y: 200});
使用场景:
1. 交换变量值
var x=1, y=2; [x, y] = [y, x]
2. 快速获取需要的对象中属性
var {hostname:domain, pathname:path} = location;
在一个对象中绑定函数,称为这个对象的方法。
var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 今年调用是25,明年调用就变成26了
绑定到对象上的函数和普通函数没什么区别,但是在它内部用了一个this,这个this就很可以拿来说说了
在一个方法内部, this是一个特殊变量,它始终指向当前对象,so
this.birth
可以拿到xiaoming
的birth
属性再拆开写
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age(); // 25, 正常结果 getAge(); // NaN
为什么getAge() 返回的是NaN呢?
这里就牵涉到this指向的问题了,
一般情况下,this指向最后调用它的对象,见上图代码,xiaoming.age(); this指向的就是对象 xiaoming, 而xiaoming是有age属性的, 所以age() 方法中的 this.birth就能取到1990
而 getAge(); 因为是直接在页面对象下定义的方法, 它的this指向的是页面对象,也就是全局对象window, 而全局对象里并未定义birth,所以就返回NaN了
是不是很神奇,更神奇的在下面
var fn = xiaoming.age; // 先拿到xiaoming的age函数 fn(); // NaN
同样是NaN
所以 想要保证this指向中具有birth 必须用obj.age(); 即xiaoming.age();才能取到
一般情况下,如果希望this总是指向某一个固定对象,可以定义全局变量,或者外部变量,比如 var _that; _that = this;
捕获该对象的this,在下方使用_that就没有问题了
修复this指向,还可以用对象原型的自有方法 call() apply() bind()
apply()
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
call()
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.call(xiaoming, ''); // 25, this指向xiaoming, 参数为空
call()和apply()的第一位参数都是修改this指向为传入对象
唯一区别就是 从第二个参数开始
call()可以有很多参数
apply()只有一个参数 且参数类型需为数组
对于普通函数调用,我们通常把this绑定为null
Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5
以上Math也可以看出apply()和call()的区别
装饰器
因为函数都是对象的属性,哪怕是页面默认函数,也可以改变其行为。
比如parseInt
现在假定我们想统计一下代码一共调用了多少次
parseInt()
,可以把所有的调用都找出来,然后手动加上count += 1
,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()
'use strict': var count = 0; var oldParseInt = parseInt; window.parseInt = function() { count += 1; return oldParseInt.apply(null,arguments); // 调用原函数 传入所有参数集arguments }
简单理解就是 将parseInt 拷贝给oldParseInt, 然后重新定义parseInt操作, 赋予 count统计 再把原来的方法给继承进去
高阶函数
简单描述,就是接受参数为函数的函数,就是高阶函数
map()
map() 遍历数组的每一项 同时接收一个函数作为处理方法,处理这遍历中的每一项
let arr = [1,2,3,4,5,6]; pow(x) { return x*x } let newarr = arr.map(pow); // newarr [1,4,9,14,25,36]
用一般的for循环同样可实现该效果
pow (x) { return x*x } let arr = [1,2,3,4,5]; let results = []; for(var i in arr) { results.push(pow(arr[i])) }
那为什么要用高阶函数map()咧?
廖雪峰老师给出了这样的解释:
从上面的循环代码,我们无法一眼看明白"把f(x)作用在Array的每一个元素并把结果返回,生成一个新的Array".
所以原因简化为下面这句话,
高阶函数把运算规则抽象化,使我们能够更清晰的看到发生了什么,和为什么会这么做。
因为运算规则被抽象化,还可以计算任意复杂的函数
比如,把Array内的所有数字转为字符串
let arr = [1,2,3,4,5,6]; let results = arr.map(String); // 因为运算规则被抽象化, 所以 轻易不要去研究这些运算过程, 在某一阶段前 没有太大的必要性
reduce()
array的归并操作,什么是归并呢? 我们先看map(), map起到的是遍历作用,遍历每一项,这里的每一项就是每一项,自身独立,和其他项没有关系,而归并,则是从数组的第一位元素开始,一直执行到最后一位,在每次执行开始,会接收对上一位元素执行的结果,举个简单的例子,求和,用reduce()求和:
let arr = [1,3,4,5,6]; let mix = arr.reduce(function(x,y) { return x + y }) mix // 25
定义,reduce()同样接收一个函数对归并项处理,但是这个函数有要求,就是必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,所以,可以简单地理解为,参数x,是当前项之前所有项的结果集合,y是归并的当前项
系统定义戳这里
廖老师的课后练习题:
修正后的代码是:
function str2int(x) { return parseInt(x) } r = arr.map(str2int);
可正常输出1,2,3
但是原题的异常原因 , 我没有找到(还是不想读英文文档hhh)
filter()
filter()的作用是过滤Array的某些元素 然后返回剩下的元素
它也接收一个函数,过滤条件就是函数内容
filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。
tips: 至于它到底有没有改变原数组,这是个令人疑惑的问题
sort()
排序在各个语言中都是极为常见的算法,很多语言都内置有排序函数sort()
js的 sort() 排序
实现核心是,
Array的sort()方法 默认把所有元素先转换为String再排序, 而对String的排序又会转换成对其对应位的ASCII码比较
直接arr.sort() 它就是这么干的
如果不想遵循它既定的规则,那么我们可以传入一个排序函数,来实现我们想要的效果
比如 按数字大小排序:
let arr = [10,20,1,2]; arr.sort(function(x, y) { if (x < y) { return -1 } if (x > y) { return 1 } return 0; }) console.log(arr); // [1,2,10,20] 至于 return -1 return 1 return 0 分别代表什么 和为什么这么return可以那么代表,请自行摸索
Array() 对象自身的高阶函数
every() find() findIndex() forEach()
every() 对arr中所有元素进行遍历, 同时接收一个条件函数, 判断arr中的每一项是否都满足该函数内条件,若全部元素都满足, 则every()执行完成后返回true, 否则返回false
find() 对arr中所有元素进行遍历,同时接收一个条件函数, 查找arr中符合条件的第一个元素,如果找到就返回这个元素,遍历完还未找到,返回undefined
findIndex()和find()类似, 不过返回的是第一个元素的索引, 无则返回-1
forEach() 和 map() 类似 相当于迭代器,在遍历过程中把每个元素依次作用于传入的函数, 不会返回新的数组,如果函数对遍历元素有修改操作,则会直接改变原数组对应元素的值
tips:
findIndex()和indexOf()的区别
findIndex的判断条件是不定的,取决于传进去的条件函数。
indexOf则是直接在原数组里检测目标元素是否存在,相当于条件唯一。
未完待续
https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016