JavaScript基础 学习记录

记录自己JavaScript基础概念完整学习的过程

本文是在学习廖雪峰老师的JavaScript教程时 所归纳的笔记

廖雪峰老师教程在这里

 注意点:

  1. file:// 开头的地址无法执行联网的JavaScript
  2. 虽然js不要强制在语句结尾加; 但是在某些情况下  让js引擎自动补全 ; 会改变程序语义
  3. js本身不限制嵌套层级,但是一般不建议嵌套太多,可以把深层代码抽出作为函数调用
  4. 注释: // 是行注释   /* */ 是块注释
  5. js严格区分大小写

数据类型和变量

     当下问数据类型就要考虑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表示值未定义。事实证明,这并没有什么卵用,区分两者的意         义不大。大多数情况下,我们都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用

     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

 Map和Set

因为基本对象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;

 

 方法(this)

在一个对象中绑定函数,称为这个对象的方法。

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可以拿到xiaomingbirth属性

再拆开写

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

97pra

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值