JS基础及基本算法

js基础

作用域

前言

都知道JS存在三种作用域,即全局,函数,块级。其实是没有块级作用域的,但我们可以通过闭包来实现它。

函数作用域

很好理解,在函数内部声明的变量在函数调用后会销毁

块级作用域

在if,while,for等代码体用{}包括的,成为块级作用域,但是在JS中没这说法。
比如

1
2
3
4
for(var i=0;i<10;i++){
console.log(i);
}
alert(i) // 9

问题出来了,按理说正常情况是控制台输出0~9,弹出undefined,但是alert出来的却是9,

那这足以说明咱们的JS是不存在块级作用域的,块里面的变量是被挂载在window上的。

那么这会引发一个问题,看下面代码:

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

这里你不管执行 第几个函数都是打印9,原因有两个,第一因为JS中不存在块级作用域,i的声明是挂载在window上的,再一个,函数在执行的时候获取到的i为循环最后一次+1的i,所以恒定为9。
解决办法:利用闭包虚构一个块级作用域,并将i的值传入。

闭包

注意事项:

  • 函数的传参是传入的复制值
  • 闭包其实就是能获取到函数内部变量的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a1 = function(){
    var n = 999;
    a2 = function(){
    n++;
    }
    return n;
    }
    console.log(a1()); // 999
    a2();
    console.log(a1()); // 1000

闭包的两个主要作用:

  • 获取函数内部的变量
  • 使函数内部的变量不被回收
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var arrs=[];
    for(var i=0;i<10;i++){
    (function(i){
    arrs[i]=function(){
    console.log(i);
    }
    })(i) //将循环的i的值传入
    }
    arrs[0] // 0
    arrs[1] // 1
    //..........

此时当函数执行的时候会拿到这个立即执行函数的i的值,而立即执行函数的值是for循环传入的,所以就能正常输出0~9。

JavaScript数据类型

分为基本类型和引用类型
值类型(基本类型):
字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol
引用数据类型:
对象(Object)、数组(Array)、函数(Function)

判断数据类型的方法

1
var arr = [];

typeof

1
2
console.log(typeof arr)   // object    对于复杂类型的对象只能返回object
console.log(typeof null) // object

instanceof

1
console.log(arr instanceof Array)   // true    可以判断变量的引用类型

Object.prototype.toString.call()

1
Object.prototype.toString.call(arr) == "[object Array]";     //基本类型和引用类型都可以得到

undefined和null的区别

null表示”没有对象”,即该处不应该有值。典型用法是:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。
  • 释放内存
1
2
3
Object.getPrototypeOf(Object.prototype)    // null
var arr = [1,2,5,41,54,]
arr = null;

undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。
1
2
3
4
5
6
7
8
console.log(typeof undefined);  //undefined
console.log(typeof null); //object

console.log(Number(undefined));//NaN
console.log(Number(10+undefined));//NaN

console.log(Number(null));//0
console.log(Number(10+null));//10

Boolean与条件判断

首先明白两点

  • 只有false,””,0,null,NaN,undefined会被转换成false。
  • 任何类型的数据在与布尔值比较时都会被转换为number类型。
1
2
3
console.log(Boolean(false));  //false

console.log(Boolean("false")); //true

其实我们的if语句的条件没有运算符的情况下就是执行了Boolean操作

1
2
3
4
5
if(x){

}
//等价于
if(Boolean(x))

那么能看懂下面的东西就对这块掌握的比较好了

1
2
3
4
5
6
7
8
console.log(Number([]));      //0
console.log([] ? true : false); //true if语句同等效果

console.log(Number({})); //NaN
console.log({} ? true : false); //true if语句同等效果

console.log([] == false); //true
console.log({} == false); //false

Symbol

Symbol是由ES6规范引入的一项新特性,它的功能类似于一种标识唯一性的ID
创建方式:
Symbol()

1
const name = Symbol('username')

特性一:每一次创建的Symbol都是唯一的,就算描述一致

1
console.log(Symbol("username") === Symbol("username"));   //  false

特性二:可以作为对象的属性,只能以[]方式访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const name = Symbol('username');   
let obj = {
[name]: '张三',
age: 18,
sex: 'male'
}
//或者
let obj = {
age: 18,
sex: 'male'
}
obj[name] = '张三';

console.log(obj[name]); //张三
console.log(obj.name); //undefined

特性三:可以被外部访问,但不能被枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const name = Symbol('username');   
let obj = {
[name]: '张三',
age: 18,
sex: 'male'
}
console.log(obj[name]); //Symbol只能以[]方式访问

Object.keys(obj) // 返回一个由obj所有自身属性的属性名(不包括不可枚举属性也不包括Symbol值作为名称的属性)组成的数组 ['age', 'sex']

for (let p in obj) {
console.log(p) // 分别会输出:'age' 和 'sex'
}

Object.getOwnPropertyNames(obj) // 返回一个由obj所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组 ['age', 'sex']

console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(username)]

Reflect.ownKeys(obj) // 返回一个由obj所有自身属性的属性名(包括不可枚举属性和Symbol值作为名称的属性)组成的数组 [Symbol(username), 'age', 'title']

全局Symbol,使用Symbol.for可以保证创建的symbol唯一

1
console.log(Symbol.for("username") === Symbol.for("username"));   //  true

Bind函数

下面是MDN的bind函数实现,这里对下面的一些写法进行剖析。

bind做了什么?

  • 调用bind,就会返回一个新的函数。
  • 新的函数里面的this就指向bind的第一个参数,同时this后面的参数会提前传给这个新的函数。调用该新的函数时,再传递的参数会放到预置的参数后一起传递进新函数。

实现:

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
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound ?
this :
oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};

// 维护原型关系
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();

return fBound;
};
}
  • Array.prototype.slice.call(arguments, 1)

    我们都知道arguments是一个类数组对象,所以这里调用数组的slice方法给arguments使用。因为arguments能像访问数组的形式进行访问(arguments[0]),且具有length属性,因此我们可以得到返回的一个有length长度的数组。
    后面传入参数1,是slice(start, end)中的一个参数start,表示从arguments的小标为1,即第二个参数开始切割。 这里是将bind函数的参数数组取出来,第一个参数不要(就是不要oThis)也就是要被绑定方法的那个对象

  • fNOP和fBound函数

    1
    2
    3
    4
    5
    //假设bind过程如下
    function a(){
    this.name = 'a';
    }
    var b = a.bind(oThis);
    • 正常调用bind函数
      返回一个新函数,该函数的this指向调用bind方法的一个参数。即代码里的:

      1
      2
      3
      4
      //bind函数内部解析
      fToBind = this; //这里的this就是a;
      fToBind.apply(oThis,........................) //这里便是将oThis替换a函数的this;
      return fBound; //即b的this便是指向oThis;

      到这里正常调用bind并传参就完成了

    • bind返回的函数作为构造函数的情况。首先我们得理解new做了什么,如下:

      1
      2
      3
      4
      5
      var o = new fun();
      //实际操作
      var o = new Object();
      o.__proto__ = Foo.prototype;
      fun.call(o);

      这里可以看到,当fun被当做构造函数执行时,fun的this指向的是o,且o可以访问fun的原型的属性。

1
2
3
4
5
6
fNOP = function () {} ;
if(this.prototype) {
// 规避Function.prototype没有prototype属性
fNOP.prototype = this.prototype; // 构造新函数,将a函数的prototype做一个中转防止原型链污染
}
fBound.prototype = new fNOP();

这样便兼容了新函数作为构造函数的情况。
这里有一个疑问,为什么新函数的要继承a函数的原型,以下是mdn的解释:

bind() 函数会创建一个新绑定函数(bound function,BF)。绑定函数是一个exotic function object(怪异函数对象,ECMAScript 2015中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。

基本算法

冒泡算法

作为经典入门算法之一,因为其每轮的比较都会确认一个最大(最小)的数,就像冒出来一样而得名。

设计思路:

  • 确定程序执行次数
  • 两两比较,按照规定交换位置

程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = [10,5,6,25,1000,1];
function bubbleSort(arr) {
for(let i = 0,len=arr.length;i<len-1;i++) {
for(let j = 0;j<len-1-i;j++) {
if(arr[j]>arr[j+1]) {
let tem = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tem;
}
}
}
return arr;
}
console.log(bubbleSort(arr)) //[1, 5, 6, 10, 25, 1000]

按照上面的数组来说就是
第一轮…….
第一次:10和5比较,交互位置,此时数组为[5,10,6,25,1000,1]
第二次:10和6比较,交换位置,此时数组为[5,6,10,25,1000,1]
…………………………………………..
第length-1次:此时数组为[5,6,10,25,1,1000]
可以看到我们已经确定了一个最大数1000,那么接下来就将[5,6,10,25,1]继续执行第二轮
第二轮…….
第length-1轮…….得到最终排好序的数组

冒泡算法效率低,我们在此基础上尽量的去优化其效率
重点解释两个地方

外层循环次数length-1
  • 按照我们的算法设计逻辑,因为每一次都会有一个数被确定,所以当执行了l-1轮,就剩最后一个数。自然是最小(最大)的,所以无需再执行
    内层循环次数length-1-i
  • 首先一个数组两两比较需要的次数为length-1次,上面我们说到每轮都会确定一个数,那么确定的数无需进行比较所以再-i

选择排序

类似与冒泡,不同的是每轮确定一个最小(最大)的数,再将其依次安放

程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const arr = [10,5,6,25,1000,1];
function selectSort(arr) {
const len = arr.length;
let minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i; //当前位置设置为最小
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //检查其他位置上有无比设定的最小位置更小的
minIndex = j;
}
}
if(minIndex != i){ //当前位置不是最小值就交换
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
console.log(selectSort(arr)) //[1, 5, 6, 10, 25, 1000]

快速排序

确定一个中间值,和其他元素进行比较,小的放左边数组,大的放右边数组。再递归左边的数组和右边的数组,最后合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr = [10,5,6,25,1000,1];
function quickSort(arr) {
if(arr.length <=1){
return arr;
}
let c = arr[0],leftArr=[],rightArr=[];
for(var i = 1;i<arr.length;i++){
if(arr[i] > c){
rightArr.push(arr[i]);
}else{
leftArr.push(arr[i]);
}
}
return [].concat(quickSort(leftArr),c,quickSort(rightArr));
}
console.log(quickSort(arr)) //[1, 5, 6, 10, 25, 1000]
Do the best!