ECAMScript-5

# ECAMScript-5

# This toturial will teach you javascript from basic to advanced

# come on,let's go get start it!

** First! Learn to use documents ** https://www.w3schools.com/js/js_whereto.asp

# some knowledges

  javascript and java are completely different languages,
  both in concept and design.JavaScript was invented by Brendan Eich in 1995,
  and became an ECMA standard in 1997.

# 基本数据类型

  • String
  • Number
  • Boolean
  • Symbol
  • Null
  • Undefined
  • BigInt for google only

# 引用类型

  • Object
  • Function
  • Array
  • Date
  • RegExp

你可以使用typeof 来查看我们的数据类型(基本类型), 但是你会发现 当你使用typeof 来检查null的时候 返回的是object 具体原因是:

  因为不同的对象在底层都表示为二进制,在javascript中二进制前三位为0的话都会被判断为object类型,
  null的二进制表示为全0,所以他的前三位肯定是0,因此 当你使用typeof来检查null的类型返回的是object。

如果使用typeof 来检查引用类型 举个栗子:

typeof Array  //function
typeof Array()  //object
typeof new Array() //object

typeof Boolean //function
typeof Boolean() // boolean
typeof new Boolean() // object

我们可以看到 typeof 不止能检查出基本数据的类型 ,还能检测出function 类型,也就是说 我们的js 内置构造函数 如Array,Number等 都是function,函数也属于对象。 而我们使用new关键字创建的实例是 一个对象Object。

typeof的安全防护机制

首先要知道undefined(未定义)和undeclared(未声明)
undefined 和 undeclared 表现相同都为undefined。
假设我们要引入一个js 但是这个js我们只想在开发环境或者测试环境中引入,这个js中有个全局开关Flag 当Flag=true时 我们认为他引入
如果直接使用if判断Flag的话 会报错。
if(Flag){//···}
因为根本没有生命这个变量
所以 我们可以使用一种安全的方式
我们可以确定 typeof Flag 存在的话 是boolean 不存在的话 或者说没有声明的话 是undefined
if(typeof Flag !== 'undefined' ){//···}

注意:

  • js是单线程
  • 代码同步执行,只有栈顶的上下文执行,其他的需要等待。
  • 全局上下文只有一个,当浏览器关闭时出栈。
  • 函数的执行上下文没有限制,理论上可以无限增加。

# 类型转换

js 可以通过一些内置的构造函数或方法来将一种类型转换成另外一种类型,如 字符串 转换成 数字

 有构造函数Number、String、Boolean、Symbol 构造函数 分别能将数据转为 数字、字符串、布尔值、标记值。
 
 let a = 12;        //  12
 let b = String(a); // '12'
 
 像这种你能够清楚的知道要转换的类型是什么的行为吧 成为显式类型转换。
 
 你知道上面的结果 那你知道下面的结果吗?
 let a = '12';
 let b = 3;
 let c = 8;
 let ret;
 
 ret = a + b;      // '123'
 ret = a - c;      // 4
 ret = c + a;      // '812'
 ret = a * b;      // 36
 ret = a + b * c;  // '1224'
 ret = a / b;      // 3
 
 看看你答对了多少?
 
 为什么会这样?
 因为在计算时由于等号两边的数据类型不同 导致cpu无法进行计算,js的编译器会自动将运算符两边的数据转换为同一类型。
 
 规则是怎样的呢?
 
 1、String字符串转换:如字符串 + 变量 = 字符串;
 如 '12' + 3 = '123';
    '' + 4 = '4';
    
 2、Number数字类型:使用算术运算符 或 关系运算符
    '12' - 3 = 9;
    '12' - '3' = 9;
    
 3、Boolean布尔转换: !变量
    !!'12' = true;
    
 4、undefined == null 但是 undefeied !== null
 
 5、NaN 不等于任何数 包括他自身
 
 6、true = 1, false = 0
 
 7、引用类型不要使用 == 判断 因为保存的是引用地址。
 
 8、'12' + 3 如何等于 15
    +'12' + 3 = 15
    9 + +'12' = ? // 21
 

# == 的作用

我们知道 1 != 2; true == true; 那是否知道 [] == ![]的结果呢? 答案是true 很神奇是吧,如果你对 == 有了一定理解之后这个就很简单啦。

首先我们来看看 == 是怎么工作的。这里会涉及强制类型转换(隐式)。

  • 1、如果有一个数据是布尔值,则在比较相等性前会将他转换成数值 即 false -- 0 true -- 1.
  • 2、如果有一个数据是字符串,另一个值是数字,在比较相等性前会先将字符串转为数字。 '' -- 0
  • 3、如果一个数据是对象,另一个不是,则会调用对象的valueOf方法,如果拿到的值不是基本类型,则会根据valueOf的返回值继续调用toString方法 (这是为什么会出现"[object object]"的原因)
  • 4、如果两个数据都是对象,则比较他们是否为同一对象。如果是 返回true 否则为 false 这两个数据在比较时要遵循以下规则
  • null 和 undefined 是相等的
  • 在比较相等性前,不能把null 和 undefined 转换成其他值
  • 如果一个数是NaN 则相等操作符会返回false ,不等操作符返回true,且NaN != NaN

这样我们就可以发现上面的例子中 ![] 将数据转换为Boolean 也就是 !Boolean([]) -- !true --> false 然后呢 等式就变成了 [] == false --> [] == 0 根据第一条和第三条来看 首先 []是对象 会调用valueOf 得到[] 不是基本类型 继续调用同String()方法 [].valueOf().toString(),得到空字符串 "" == 0 然后再根据第二条 Number("") == 0 --> 0 == 0 --> true

这个我们会了 再来个 [undefined] == false. 你会发现,当我们熟悉这个规则之后是very的easy,very的简单。


# Scope 作用域

我的理解 作用域类似于一个集合,这个集合显示出的是我们函数或者变量能够访问到的值。 打个比方说 如果有一个变量a在全局作用域中创建 然后 有一个变量b 在函数foo中创建,然后我们把a的值赋给b,然后我们在foo函数外打印出b,此时会发生什么?

var a = 1
function foo(){
  var b = a;
}
console.log(b)

会报错 b is not defined 对吧 为什么呢? 我们来看一下这个流程 在全局作用域中 我们能访问到的是不是 { a, foo } 这个 然后呢 我们再看看 foo 的作用域 { b } 但是 我们的 foo 函数是处于全局作用域下的 所以 他能访问到的作用域 是 [{b},{a,foo}] 这两个 当我们给 b 赋值 a 时 由于 a 在我们能找到的范围中。 但是 当我们想要在函数外打印出 b 的时候我们发现 b 只能在foo中才能找到,可是 我们打印实在foo函数外面 所以找不到变量 b 于是就报错了。 而 foo 访问的作用域 称为作用域链 ScopeChain:[VO(foo),VO(global)]. 可能会有人认为 我们的 foo 的作用域是含于 全局作用域的,这样是错误的。 你可以认为形成了一条单向的通道供我们来访问外部的作用域,事实上 是会按照 VO 这个变量对象创建的顺序来访问的 也就是按照作用域链的顺序 如 在foo函数中 我们给变量b赋值为a 作用域链是[{b},{a,foo}] 这个时候首先查找了自己的作用域 发现没有 a 于是会继续向上查找 发现 诶全局作用域是有的,然后就拿过来用了。

# 闭包

可参考此文章 (opens new window)
闭包是面试时候面试官最喜欢问的面试题之一吧,听说闭包是能将个二十分钟到半个小时的,我们一起来研究一下。争取咱也来个半小时。 闭包是一种特殊的对象。 由两个部分组成,一个是函数创建的执行上下文 代号 Alpa 及 在这个执行上下文中创建的函数 代号Beta。 当Beta这个函数执行并开始访问Alpa中的变量对象的值时,就会发生闭包。 如下: 我们定义了一个函数foo 并 创建了 a,b 两个变量并赋值,然后在foo 函数内 又定义了一个函数 boo 最后通过 foo 把 boo 函数返回出来。

  function foo(){
    var a = 10;
    var b = 20;
    
    function boo(){
      a ++;
      b ++;
      return a + b; 
    }
    return boo;
  }
 var sn1 =  foo()
 sn1();

我们来分析一下这个函数 在全局作用域中 作用域是 { foo,sn1 } 在foo作用域中 作用域是 { a,b,boo } 作用域链 [{a,b,boo},{foo,sn1}] 在boo作用域中 因为没有定义新的函数或变量 所以自身的作用域为 { } 但是 boo 是在foo作用域下创建的 自然能够访问到 foo 的作用域 于是 作用域链 [{a,b,boo},{foo,sn1}] 虽然 foo 这个函数执行会得到 function boo(){a++;b++;return a+b} 这个函数 但是我们的作用域链中是可以访问到 a 和 b 的值的 所以并不会出现爆红的现象。 如果我们执行boo 就会访问到foo 的内部变量 a,b 这个时候就会产生闭包。 也就是说当 foo 执行完毕 且已经向外部返回了函数 boo 之后 他的执行上下文确实已经出栈销毁,但是他的激活对象AO并没有销毁,因为boo还保存着对foo内部变量的引用,所以foo 的AO 会一直存在内存中。

var func = null

function handler() {
    var a = 0;

    function counter() {
        return a++;
    }

    func = counter
}

handler();
console.log(func()); //0
console.log(func()); //1
console.log(func()); //2

我们也可以通过闭包来保存我们之前的状态 如上面这个计数器 虽然 counter 被保存在全局变量func中,但是实际上 counter的作用域还是指向 函数 handler 的 我们只是将 counter 的访问地址付给了全局变量func

闭包的应用:

  • 函数柯里化 形如 foo()()
  • 模块

有一个很简单很常见的例子 就是说 如何 让 下面的执行结果是按顺序的

for (var i = 0; i < 5; i++) {
        setTimeout(()=>{
            console.log(i);
        },1000)
}

我们可以发现 当你运行这段代码的时候 执行结果是 5 全都是 5。

//1、可以将 var 改为 let 这样块级作用域就不会出现这种问题
for (let i = 0; i < 5; i++) {
        setTimeout(()=>{
            console.log(i);
        },1000)
}

//2、可以使用立即执行函数,我们可以将 i 作为参数传入到立即执行函数中 此时
for (var i = 0; i < 5; i++) {
    (function (s) {
        setTimeout(()=>{
            console.log(s);
        },1000)
    })(i)
}

切记闭包不是真的只让你回答现象,包括了闭包的产生,作用域,作用域链,执行上下文,VO,AO

《javascript高级编程》中有句话是这么说的:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的资源,过度使用闭包,会导致内存占用过多,造成内存泄漏。

所以要及时的将不用的空间释放。


# this 绑定

  • 默认情况(严格模式/非严格模式)
  • 隐式绑定
  • 显式绑定
  • new关键字绑定
  • 箭头函数(ES6)

# 默认情况

非严格模式: 在非严格模式下 this 默认指向全局对象 严格模式: 严格模式下,this 会指向 undefined。在严格模式下嗲用函数不会影响默认绑定。 如下 1:a 默认为全局变量 运行test函数 此时能够打印出结果1 this 默认情况下指向全局对象 如下 2:当我们将此函数运行在严格模式时,此时会发现爆红,a is not defined,因为严格模式下不能使用默认情况,此时的 this 指向的是 undefined。

// 1
a = 1
function test() {
    console.log(this.a);
}

test()

// 2
'use strict';
a = 1
function test(){
    console.log(this.a);
}
test()

# 隐式绑定

当函数的引用有上下文对象,隐式绑定规则会把函数中的this绑定到这个上下文对象。 如下: 当我们想要使用对象中的name属性时

var user = {
    name: 'lilei',
    sayHi: sayHi
}

function sayHi() {
    console.log('hello, my name is' + this.name);
}

user.sayHi();

但是这样的绑定是不牢固的,在某些特定情况下可能会丢失this的绑定 这种情况叫做隐式丢失 被隐式绑定的函数,如果将引用传递给一个新的变量,那么此时就相当于直接调用函数本身,this 指向 全局对象或 undefined

var user = {
    name: 'lilei',
    sayHi: sayHi
}

name = 'hanmeimei'

function sayHi() {
    console.log('hello, my name is ' + this.name || '空');
}

var handler = user.sayHi;
handler()

如下:同上面的情况相同,本质都是直接引用了函数本身,但是通过回调函数传递的方式,实质上相当于变量赋值。

var user = {
    name: 'lilei',
    sayHi: sayHi
}

name = 'hanmeimei'

function sayHi() {
    console.log('hello, my name is ' + this.name || '空');
}


function main(func) {
    func()
}

main(user.sayHi)

显式绑定: javascript中可以通过call,apply方法来改变 this 的指向。 call(target,...rest) 第一个参数是我们要指向的目标 ,然后是我们 函数需要接受的参数 如下 我们定义了一个对象 user 合一个方法 sayName 来输出 对象 user 的名称信息 然后通过 call 方法来改变 this 的指向让其指向 user

var user = {
    name: 'zhangsan'
}

function sayName(age, hobbies) {
    console.log('hello my name is ' + this.name + ' ' + age + ' ' + hobbies.reduce((prev, item) => prev + ',' + item), '');
}
// hello my name is zhangsan 10 football,basketball,swimming 

sayName.call(user, 10, ['football', 'basketball', 'swimming'])

apply(target,[...rest]) apply方法和call的功能类似,但是apply如果想要给函数传递参数的话,需要以数组的形式传参 如下:

var user = {
    name: 'zhangsan'
}

function sayName(age, hobbies) {
    console.log('hello my name is ' + this.name + ' ' + age + ' ' + hobbies.reduce((prev, item) => prev + ',' + item), '');
}

sayName.apply(user, [10, ['football', 'basketball', 'swimming']])

在隐式绑定的时候会出现丢失this指向的情况。如果我们更换了显示绑定也就是 call、apply还会不会出现呢?

bind bind(target) ES5中内置了 Function.prototype.bind bing会返回一个绑定了target的函数

var user = {
    name: 'zhangsan'
}

var user1 = {
    name: 'linken'
}

function sayName(age, hobbies) {
    console.log('hello my name is ' + this.name + ' ' + age + ' ' + hobbies.reduce((prev, item) => prev + ',' + item), '');
}

sayName.apply(user, [10, ['football', 'basketball', 'swimming']])
sayName.bind(user1)(20, [1, 2, 3])

# native code

我们可以尝试着去实现一下 bind 首先我们可以看一下 bind 的特征

  • bind可以被我们的函数调用 说明 在Function原型中
  • 调用bind 需要传入一个参数target
  • 返回一个已经绑定了target的函数
  • 这个返回的函数可以继续传入参数来调用
Function.prototype.myBind = function (target) {
    var _this = this
    return function () {
        _this.call(target, ...arguments)
        // 或是 _this.apply(target,arguments)
    }
}

var user = {
    name: 'zhangsan'
}

function hello(...rest) {
    console.log(this, rest);
}

var handler = hello.myBind(user)
handler(1, 2, 3, 4, 5)

一些内置的api也是有能够改变this指向的参数的 比如:下列代码,这个参数的功能和call 与 apply是

var user = {
    name: 'zhangsan'
}
var arr = [1, 2, 3, 4, 5]
arr.forEach(function () {
    console.log(this);
}, user)

new关键字来绑定 情人节的时候,我们都听过一个梗叫做 new 一个对象出来 那个被 new 的东西叫做构造函数 但是。

  • js中 ,构造函数只是使用 new 操作符时被调用的 普通函数 (意思就是谁来都一样,只不过后来规定了构造函数需要首字母大写),他们不属于类,也不会实例化类(js是没有类这个概念的,虽然ES6定义了class)
  • js中内置对象函数也是可以用 new 来调用的 这种称为构造函数调用。
  • 并没有构造函数这个东西,只有构造调用

使用 new 来调用函数,会进行一些操作:

  • 创建一个新对象。
  • 将新对象的原型指向构造函数
  • 将新对象绑定为构造函数的this
  • 如果构造函数返回了其他对象则 不变 否则 返回新对象!

根据上面的步骤我们来模拟一下 new 关键字

function myNew(constructor, ...rest) {
    let obj = {};
    obj.__proto__ = constructor.prototype;
    let ret = constructor.apply(obj, rest);
    return ret instanceof Object ? ret : obj
}

function Person(name, age) {
    this.name = name;
    this.age = age;
}

p = myNew(Person, '张三', 18);
console.log(p);
let obj = myNew(Object)
console.log(obj);

测试正常! 代码解析 首先创建一个新对象,然后将新对象的原型指向了构造函数,这样我们的对象就能够访问构造函数中的属性,然后 通过 apply 将 对象绑定到了构造函数中 也就是说 对象和构造函数真正的关联起来 此时 构造函数中的 this 指向了 对象 ,也就相当于 obj['name'] = name;obj['age'] = age; 然后对外返回这个对象或者是构造函数中的对象。

如果 显示改变this指向时传入的是 null 那么默认绑定规则会把this绑定到全局对象中。

更安全的做法是 传入一个空对象,把this绑定到这个对象不会有任何副作用。 JS中创建一个空对象的方式是Object.create(null) 他会创建一个空对象,但是不会创建Object.prototype 所以 比直接赋值为{}更空 更安全。

间接引用:

function test() {
    var a = 1;
    var b = { a: 2, foo: foo };
    var c = { a: 3 };


    console.log(b.foo()); // 2

    var s = (c.foo = b.foo)()
    console.log(s);
}

function foo() {
    console.log(this);
    return this.a
}

a = 10

test()

(c.foo = b.foo)() 相当于直接执行foo函数 this 指向全局对象。

Function.prototype.myBindTwo = function (obj) {
    var _this = this
    var arr = [].slice.call(arguments, 1)
    var bind_fun = function () {
        return _this.apply((!this || this === (window || global)) ? obj : this, arr.concat.apply(arr, arguments))
    }
    bind_fun.prototype = Object.create(_this.prototype)
    return bind_fun;
}
---------
function foo() {
    console.log("name:" + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 

// 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!

// 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!

// 绑定丢失,应用软绑定
setTimeout( obj2.foo, 10 ); // name: obj