你真的学会JavaScript了吗?
请听题,请在评论区或在心中给出以下代码的执行结果:

var arr = [];
for(var i = 0; i < 3; i++) {
    arr.push(function() {console.log(i)});
}
arr[0]();
arr[1]();
arr[2]();
parseInt("678tianlangstudio");
+"678tianlangstudio";
1 + "678tianlangstudio";

写好答案了吗?要公布答案了哦


0

1

2

NaN

678tianlangstudio

1NaN

如果你的答案跟上面的任意一个匹配上了,那恭喜你! 可以一起往下面看了,因为这是一份全错的答案。

想起上高中时有次英语老师拿了张考卷对着答案讲了半天,然后对我们说:

这是B卷看成A卷的答案了,我们从头讲。

从此我就再不相信什么答案了,甚至遇到做不出的题我都怀疑是不是题出错了!慢慢的养成了独立思考的习惯,不知是好是坏。感谢老师苦心一片,至今教诲良言时犹在耳:

答案错了,重新开始

类型

JavaScript是动态类型语言,也就是说在代码编写时不需要声明指定其类型,变量类型在代码运行时才确定.

基本类型

没有方法、不可变

包括:

  1. undefined
  2. null
  3. boolean
  4. number
  5. string
  6. (symbol)

复杂类型

  1. 对象(包括数组)
  2. 函数

可以使用typeof判断数据类型:

typeof "Bill"              // 返回 "string"
typeof 3.14                // 返回 "number"
typeof true                // 返回 "boolean"
typeof false               // 返回 "boolean"
typeof x                   // 返回 "undefined" (假如 x 没有值)
typeof {name:'Bill', age:62} // 返回 "object"
typeof [1,2,3,4]             // 返回 "object" 
typeof null                  // 返回 "object"
typeof function myFunc(){}   // 返回 "function"

需要注意:

typeof null 返回object 历史原因兼容原来的版本

typeof arr 返回 object 数组也是对象

typeof function testFun() {} 返回 function

类型转换

数据从一个类型转换到另一个类型

强制类型转换和隐式类型转换

强制就是你得自己编写代码转换数据类型,隐式的就是有执行引擎帮你转换.

const x = 17;
const explicit = String(x); //使用强制类型转换把数字x转换为字符串explicit 
const implicit = x + ""; //引擎看到你要把个数字跟字符串相加,就帮你把数字转为字符串了.测试题最后一行类似.

操作符== 和 ===

== 会把两边的数据转换为同一种类型再比较是否相等,也就是忽略数据类型,比如:

1 == '1';//true

=== 先判断数据类型,数据类型不一样就直接false, 不为其类焉问其值, 比如:

1 === `1`; //false

会被隐式转换为false的类型

  1. undefined
  2. null
  3. false
  4. +0, -0, NaN
  5. "" //空字符串

注意:

'false' -> true

会被隐私转换为true的类型

此处省略10000字

太多,因为除了上面的都是^-^

基本数据类型(Primitives) VS 复杂数据类型(Objects)

  1. 基本数据类型都是不可变的. 复杂数据类型可变并且使用索引操作
  2. 基本数据类型值传递,复杂数据类型使用引用传递(做为函数、方法参数时)

原型链

  1. 非基本数据类型都有关联属性和方法,比如:

    Array.prototype.push()

    String.prototype.toUpperCase()

注意:

String是复杂类型,'tianlang'这个是基本数据类型,有些时候它们行为一样,这是因为有自动转化,专业俗语自动装箱. 例如:

1778.toString();//会报错,因为基本类型没有方法。这么粗暴直接冒失的调用执行引擎也不好意思先装个箱
const x = 1788; 
x.toString(); //"1788"  斯文多了,自动装箱成对应的复杂类型走起。
x.__proto__;//Number...

基本数据类型对应的装箱复杂类型:

String()

Number()

Boolean()

Object()

(Symbol())

Java里也有这个概念,但JavaScript除了名字跟Java没半点关系.

  1. 每个对象都持有一个原型的引用
  2. 跟对象关系越近的原型上定义的属性或方法优先级越高

作用域(Scope)

作用域就是变量生效的范围

使用var定义的变量有效范围是从定义开始到所在函数结束.

使用const,let 定义的变量有效范围是从定义开始到所在块结束.

定义提升:

​ 这个需要先说下程序执行过程:

  1. 执行引擎读取整个程序脚步
  2. 解析判断是否有语法错误,如果有错误报错退出执行
  3. 把函数保存到内存中
  4. 声明使用var定义的变量(注意:只有声明没有初始化赋值)

  5. .......

    这就是为什么,我们可以先调用一个函数后对这个函数进行定义,可以先使用一个使用var定义的变量,后面才使用var定义变量而不会报错。因为再执行时把函数定义的代码和var定义提升了.注意看3,4.

全局对象

可以把全局对象想象成一颗大树,在程序中定义的变量也好函数也好其实都挂在一个全局对象上。

在浏览器运行环境中,全局对象是window

在Node.js运行环境中,全局对象是global

闭包

咋一看,不知道是做什么的,再一看还是不能从名字看出这货具体是做什么的.看下定义:

可以用来获取父函数中定义的变量的函数.

小蒙怡情大蒙伤身,还是看下代码,回头看我们前面提到的测试题1:

var arr = [];
for(var i = 0; i < 3; i++) {
    arr.push(function() {console.log(i)});
}
arr[0]();
arr[1]();
arr[2]();

输出的结果是:

3

3

3

意不意外?

要一次性理解这个可能有点难,我们先来个简单的:

function makeHelloFunction() {
  var message = 'Hello!'

  function sayHello() {
    console.log(message)
  }

  return sayHello
}

const sayHello = makeHelloFunction()
console.log('typeof message:', typeof message) //undefind 因为message是定义在函数makeHelloFunction内部的,函数外边是不能访问的.
// but the function sayHello still references a variable called message

sayHello() //sayHello方法却可以打印出message的值,因为sayHello函数是定义在makeHelloFunction内部的.

看了上面的例子是不是对这货为什么叫闭包有了些许的感悟。因为ES6前只能使用var定义变量,而这货定义的变量作用域本来就比较宽还有定义提升就更容易造成变量的作用域污染了.

什么是变量的作用域污染呢?

你定义了个变量name叫张三, 你的同事或者你在其它地方无意识的又定义了name叫李四,后来就变成了你以为的张三不知道怎么就变成了李四了.

为了定义新变量name叫李四时不影响原来的张三,于是我们可以把李四关起来定义,关起来也就是封起来。就像上面的演示代码,只有在函数中定义的函数sayHello才能访问到函数中定义的变量message,message对外部是不可见的,也就不会影响外部原来定义的变量.

上例中使用sayHello时还需要先调用makeHelloFunction创建,如果每次都这样岂不是挺麻烦的?

我们可以使用立即执行函数定义方式,就是来个小括号,后面的小括号里还能传递参数.就项这个样子:


const sayHello = (function makeHelloFunction() {//这里的函数名makeHelloFunction没有用了,可以删除掉
  var message = 'Hello!'

  function sayHello() {
    console.log(message)
  }

  return sayHello
})()
console.log('typeof message:', typeof message) //undefind 因为message是定义在函数makeHelloFunction内部的,函数外边是不能访问的.
// but the function sayHello still references a variable called message

sayHello() //sayHello方法却可以打印出message的值,因为sayHello函数是定义在makeHelloFunction内部的.

到此是不是还不知道为啥子测试1里输出的是3,3,3而不是0,1,2? 没关系,可以先让它输出0,1,2.

var arr = [];
for(var i = 0; i < 3; i++) {
    arr.push((function(j) {
       return function() {
             console.log(j)
       }
    })(i));//这里我们用了闭包和立即执行函数捕获当前变量i
}
arr[0]();
arr[1]();
arr[2]();

这下明白了吧,可以运行下这段代码看下效果,如果还是不明白也可以加群讨论.

函数是一等公民

那什么时候, 人分三流九等,士农工商。但是人生而平等嘛,怎么体现平等呢?基本的权利大家应该都有吧.就像函数,虽然长得跟普通对象啊数字啊不一样,但是收到的待遇却是差不多地。可以定义变量把一个函数赋值给它,也可以把函数做为另一个函数的参数使用,这个就厉害了,可以实现很多高级的功能.也称这种参数是函数的函数为高阶函数.

同步?异步?单线程?

  • JavaScript是单线程同步执行的语言.

  • 如果一个函数执行的时间比较长就会引起页面的卡顿,比如在运行个这样的函数,再去点击页面里的按钮你会发现没得反应了:

    function hang(seconds = 5) {
    const doneAt = Date.now() + seconds * 1000
    while(Date.now() < doneAt) {}
    }
  • 但是有些函数是可以异步执行的,比如:

    • setTimeout()
    • XMLHttpRequest(), jQuery.ajax(), fetch()
    • 调用数据库

    是不是很好奇JavaScript怎么即是同步单线程的语言又支持异步呢?这主要是内部维护了一个任务队列,如果关于这块您有什么想法也可以给加群给大家分享.

    回调

​ 原来处理异步代码的方式是添加回调函数,异步代码执行完成后会触发回调函数。比如这样:

function login(req, res, callback) {
  User.findOne({email: req.body.email}, function(err, user) {
    if (err) return callback(err)

    user.comparePassword(req.body.password, (err, isMatch) => {
      if (err) return callback(err)
      if (!isMatch) return res.status(401).send('Incorrect password')

      // add relevant data to token
      const payload = {id: user._id, email: user.email}

      jwt.sign(payload, config.secret, {}, function(err, token) {
        if (err) return callback(err)

        user.token = token
        user.save((err) => {
          if (err) return callback(err)
          res.json({token})
        })
      })
    })
  })
}

异步函数多了,我们需要一层一层的嵌套回调函数,就成了回调黑洞,这样的代码读起来麻烦,维护起来也麻烦,一不小心就不知道哪里少敲了个括号. 于是就引入了Promise编程模型,减少回调嵌套,就像这个样子:

fetch(url)
  .then(function(res) {
    return res.json()
  })
  .then(function(json) {
    return ({
      importantData: json.importantData,
    })
  })
  .then(function(data) {
    console.log(data)
  })
  .catch(function(err) {
    // handle error
  })

是不是清爽了很多?

后来ES2017又新增了async/await关键字用于支持异步编程,就像这样:

async function login(req, res, callback) {
  try {
    const user = await User.findOne({email: req.body.email})
    const isMatch = await user.comparePassword(req.body.password)

    if (!isMatch) return res.status(401).send('Incorrect password')

    const payload = {id: user._id, email: user.email}
    const token = await jwt.sign(payload, config.secret, {})

    user.token = token
    const success = await user.save()

    res.json({token})
  } catch (err) {
    callback(err)
  }
}

是不是跟Rust Async有点像? 学语言嘛,这也是为什么我总是劝新同学要学一门语言学通再学其它的。语言嘛总有相通之处,虽不能一通百通但也是有大量可复用之处的.

this

初接触Javascript会觉得this真是飘忽不定,特别是在事件处理时使用到this,常常搞不清它这个纠结指的是那个;

这里总结几条规则:

  1. 函数中的this指向函数的调用时所在对象,如:

    obj.fun();// fun中的this指向boj

  2. 如果没有对象那在严格模式下this就指向全局对象windows或者global

  3. 可以使用bind,call, apply显示绑定this到某个对象.

该煮饭了,有时间再单独写篇this ,欢迎关注