JavaScript 难点梳理(上)

这篇文来源于一次小范围分享,分享成员想听我讲一些关于JavaScript和其他语言不一样的地方,对于我这个JavaScript做为主要语言的码农,虽然每天都在写但也确实没有深入研究过它。解决一些匪夷所思全是凭借着其他语言的经验加上Google的帮忙。趁着这个机会我好好整理下JavaScript的一些难懂点和与其他语言不一样的点。因为文章都是各种知识点拼凑而成,可能不一定读的流畅,我会在后面慢慢优化它。让我们开始吧!

函数

隐式函数参数

  • this
  • arguments

arguments是类数组类型,不是数组,而剩余参数(...args)是数组。

原型和原型链

在了解原型和原型链之前我们先了解一下构造函数的相关知识。

构造函数

下面是一个简单的构造函数的用法

function Foo(name,age){
    this.name=name
    this.age=age
    this.class='class-1'
    // return this 默认有这一行
}

var f =new Foo('zhangsan',20)

在JavaScript中:

  • var a={} 其实是 var a= new Object()的语法糖
  • var a=[] 其实是 var a= new Array()的语法糖
  • function Foo(){...} 其实是var Foo=new Function(...)
  • 使用instanceof 判断一个函数是否是一个变量的构造函数

原型对象

理解了构造函数,我们来了解下原型对象的相关概念。

  • 所有的引用类型(数组、对象、函数)都具有对象特性,既可自由扩展属性,null 除外

一个可能永远不会修复的bug:typeof null === "object"

var obj={};obj.a=100;
var arr=[]; arr.a=100;
function fn (){};
fn.a=100;
  • 所有的引用类型(数组、对象、函数)都有一个“__proto__”的预定义属性,属性值是一个普通的对象(我们也叫它隐式原型)
var obj={};obj.a=100;
var arr=[]; arr.a=100;
function fn (){};
fn.a=100;

console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn.__proto__);

  • 所有的函数都有一个“prototype” 预定义属性,属性值也是一个普通的对象(我们也叫它显式原型)
var obj={};obj.a=100;
var arr=[]; arr.a=100;
function fn (){};
fn.a=100;

console.log(fn.prototype);

  • 所有的引用类型(数组、对象、函数) “__proto__”属性值指向它的构造函数的“prototype”属性值
var obj={};obj.a=100;
var arr=[]; arr.a=100;
function fn (){};
fn.a=100;

console.log(obj.__proto__===Object.prototype);
console.log(arr.__proto__===Array.prototype);
console.log(fn.__proto__===Function.prototype);

如果我们要循环对象的属性,我们可能即得到了对象自身的属性,也可以得到来自原型的属性,但一般我们希望只得到对象自身的属性,可以使用f.hasOwnProperty(item)去屏蔽原型属性。

在某些IDE中使用for in会自动加上hasOwnProperty
高级浏览器在for in的时候已经自动屏蔽了来自原型的属性。

原型链

//构造函数
function Foo(name,age){
    this.name=name;
}

Foo.prototype.alertName=function(){
    alert(this.name);
}

var f=new Foo('zhangsan');

f.printName=function(){
    console.log(this.name);
}

f.printName();
f.alertName();
f.toString()

f.toString()会查找f的隐式原型即f.__proto__如果找不到,会继续查找'f.__proto__.__proto__'即Object的prototype。

这个查找的过程就是原型链。

通过原型实现继承的例子:

            function Person(){}
            Person.prototype.dance = function(){};

            function Ninja(){}
            Ninja.prototype = new Person();

            const ninja = new Ninja();
            assert(ninja instanceof Ninja,"ninja receives functionality from the Ninja prototype" );
            assert(ninja instanceof Person, "... and the Person prototype" );
            assert(ninja instanceof Object, "... and the Object prototype" );

原型和原型链总结

作用域

对于JavaScript作用域,我们首先要记住以下两点:

  • js没有块级作用域
  • 只有全局作用域和函数作用域

this

JavaScript中的this和其他语言中的this不太一样。

  • this 要在执行时确认值,定义时无法确认
  • 函数的调用方式不同,this不同。

下面我们详细了解下函数调用方式不同this的不同。

作为一个函数直接调用 skulk()

当以这种方式调用时 函数的上下文(this)有两种可能,在严格模式下,它将是全局上下文(window 对象),而在严格模式下,它将是undefined

  //作为函数直接被调用

  function ninja(){
    return this;
  }

  function samurai(){
    "use strict";
    return this;
  }

  assert(ninja()===window,`In a 'nonstrict' ninja function,
  the context is the global window object`);

  assert(samurai()===undefined,`In a 'strict' samurai function,
  the context is undefined`);

作为一个方法调用 ninja.skulk()

当函数作为某个对象的方法被调用时,该对象会成为函数的上下文(this)

   function whatsMyContext() {
      return this;
    }

    var ninja1 = {
      getMyThis: whatsMyContext
    };

    assert(ninja1.getMyThis() === ninja1,
      "Working with 1st ninja");

    var ninja2 = {
      getMyThis: whatsMyContext
    };

    assert(ninja2.getMyThis() === ninja2,
      "Working with 2nd ninja");

作为一个构造函数new Ninja()

当调用构造函数时会发生一系列特殊的操作:

  1. 创建一个新的空对象
  2. 该对象作为this参数传递给构造函数,从而成为构造函数的函数上下文。
  3. 新构造的函数作为new运算符的返回值

当函数作为构造函数调用时,该函数的上下文(this)是一个新创建的对象实例。

    function Ninja() {
      this.skulk = function () {
        return this;
      };
    }

    var ninja1 = new Ninja();
    var ninja2 = new Ninja();

    assert(ninja1.skulk() === ninja1,
      "The 1st ninja is skulking");
    assert(ninja2.skulk() === ninja2,
      "The 2nd ninja is skulking");

如果构造函数中有返回值会怎样?
- 如果构造函数返回一个对象,则该对象将作为整个表达式的值返回,而传入该构造函数的this将丢弃
- 如果构造函数返回的是非对象类型,则忽略返回值,返回新创建的对象。

    function Ninja() {
    this.skulk = function () {
      return true;
    };

    return 1;
  }
  assert(Ninja() === 1,
    "Return value honored when not called as a constructor");

  var ninja = new Ninja();

  assert(typeof ninja === "object",
    "Object returned when called as a constructor");
  assert(typeof ninja.skulk === "function",
    "ninja object has a skulk method");
    var puppet = {
      rules: false
    };

    function Emperor() {
      this.rules = true;
      return puppet;
    }

    var emperor = new Emperor();

    assert(emperor === puppet,
      "The emperor is merely a puppet!");
    assert(emperor.rules === false,
      "The puppet does not know how to rule!");

通过函数的apply或者call方法调用skulk.call(ninja)

通过apply或者call方法可以指定函数上下文。

this指向callapply的第一个参数。

    function juggle() {
      var result = 0;
      for (var n = 0; n < arguments.length; n++) {
        result += arguments[n];
      }
      this.result = result;
    }

    var ninja1 = {};
    var ninja2 = {};

    juggle.apply(ninja1, [1, 2, 3, 4]);
    juggle.call(ninja2, 5, 6, 7, 8);

    assert(ninja1.result === 10, "juggled via apply");
    assert(ninja2.result === 26, "juggled via call");

执行上下文栈和作用域链

JavaScript代码有两种类型,一种是全局代码,在所有函数的外部定义,一种是函数代码,位于函数内部。

既然有两种代码类型也就有两种上下文,全局执行上下文和函数执行上下文。

全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次函数调用时就会创建一个新的,并压入函数上下文栈,此外还会创建一个与之相关联的词法环境,在变量查找时,会顺着环境往外层查找,直到找到这个变量。这种变量查找方式也就构成了作用域链

var a =100;
function F1(){
    var b=200;
    function F2(){
        var c=300;
        console.log(a); // 100
        console.log(b); // 200
        console.log(c); // 300
    }
    F2();
}
F1();

上下文堆栈入栈和出栈过程

根据JavaScript上下文栈和词法环境的特点,我们将代码稍作更改一样能得到结果。

console.log(a);// 100
function F1(){
    var b=200;
    function F2(){
        var c=300;
        console.log(a); // 100
        console.log(b); // 200
        console.log(c); // 300
    }
    F2();
}

var a=100;
F1();

这是JavaScript语言和其他语言不同的地方,如C#和java声明变量必须在使用变量前,JavaScript变量只要在同一个或者父级词法环境(顺着作用域链可找到)即可。

C# 在声明前使用变量会报错

闭包

闭包允许函数访问并操作函数外部的变量。只要变量或函数存在于声明函数时的作用域,闭包即可使函数能够访问这些变量或函数。

闭包的形成是因为每定义一个函数都会产生一个作用域,这个作用域会跟着这个函数,即使运行时函数的作用域不存在,依旧可以访问定义时函数作用域中的变量。

    var outerValue = "samurai";
    var later;

    function outerFunction() {
      var innerValue = "ninja";

      function innerFunction() {
        assert(outerValue === "samurai", "I can see the samurai.");
        assert(innerValue === "ninja", "I can see the ninja.");
      }

      later = innerFunction; // 将内部函数innerFunction的引用存储在变量later上
    }

    outerFunction();
    later();

我们来看一下这段代码在执行时的上下文堆栈情况,这次我们分析每个函数的闭包。

在调试状态下的执行情况:注意观察堆栈、作用域和闭包(Closure):

  • 第一步

  • 第二步

  • 第三步

当在outerFunction中声明innerFunction时,不仅定义了函数的声明,还创建了一个闭包。
该闭包不仅包含了函数声明,还包含了声明时该作用域的所有变量。
当最终执行innerFunction时,尽管声明时的作用域已经消失了,但通过闭包,仍然能够访问原始作用域。

闭包的使用

  • 封装私有变量
  function Ninja() {
      var feints = 0;
      this.getFeints = function(){
        return feints;
      };
      this.feint = function(){
        feints++;
      };
     }

     var ninja1 = new Ninja();
     ninja1.feint();

     assert(ninja1.feints === undefined,
            "And the private data is inaccessible to us.");
     assert(ninja1.getFeints() === 1,
           "We're able to access the internal feint count.");


     var ninja2 = new Ninja();
     assert(ninja2.getFeints() === 0,
            "The second ninja object gets it’s own feints variable.");

在构造器内部,我们定义了一个变量 feints用于保存状态,由于JavaScript的作用域规则限制,因此只能在构造器内部访问该变量。为了能让作用域外部的代码能够访问该变量,我们定义了访问该变量的方法getFeints

  • 回调函数
<!DOCTYPE html>
<html>
<head>
  <title>Using a closure in a timer interval callback</title>
  <meta charset="utf-8">
  <script src="../assert.js"></script>
  <link rel="stylesheet" type="text/css" href="../assert.css">
  <style>
    #box1{
        width: 100px;
        height: 100px;
        position: relative;
        margin:5;
        color: white;
        font-weight: bolder;
      background-color:blue;
      margin-bottom: 100px;
    }
  </style>
</head>
<body>
    <div id="box1">First Box</div> 
  <script>
    function animateIt(elementId) {
      var elem = document.getElementById(elementId);
      var tick = 0;
      var timer = setInterval(function(){
        if (tick < 100) {
          elem.style.left = elem.style.top = tick + "px";
          tick++;
        }
        else {
          clearInterval(timer);
          assert(tick === 100,
                 "Tick accessed via a closure.");
          assert(elem,
                 "Element also accessed via a closure.");
          assert(timer,
                 "Timer reference also obtained via a closure." );
        }
      }, 10);
    }
    animateIt("box1");
  </script>
</body>
</html>

ES6对作用域的改进(搅屎棍)

var vs. let const

  • 当使用关键字var的时候 该变量是在距离最近的函数内部或全局词法环境中定义,没有块作用域。
  • 当使用let和const时就定义了一个具有块级作用域的变量。
for(var i=0; i<3;i++){
    var a=1;
    console.log(i); // 0、1、2
}

console.log(i); // 3
console.log(a); // 1

for(let i=0; i<3;i++){
    let a=1;
    console.log(i); // 0、1、2
}

console.log(i); // ReferenceError
console.log(a); // ReferenceError

使用箭头函数绕过函数上下文

如上述函数的上下文根据函数的调用方式不同而不同。因此,ES6又来改进了。

箭头函数没有单独的this值,箭头函数的this与声明所在的上下文相同。

调用箭头函数时,不会隐式的传入this参数,而是从定义时的 继承上下文。箭头函数中的 this 值始终来自闭包所在的作用域。

var arrobj = {
      yoshi: true,
      arrfun : () => {
        assert(this.yoshi, "In arrow function we can not access yoshi");
      },

      arrfun2: function(){
        assert(this.yoshi, "But In function we can access yoshi");
      }
    }

    arrobj.arrfun();
    arrobj.arrfun2();

作用域总结

对于JavaScript 作用域,我们需要理解作用域链,知道如何根据作用域链查找变量,还有函数调用方式不同上下文的不同,遇到es6语法(let const 箭头函数等),自动将其转换成块级作用域。这样基本解决了大部分JavaScript作用域的问题。

闭包是JavaScript作用域规则的副作用。通过闭包可以访问函数创建时所处环境的全部变量,即使函数创建时所在作用域消失,仍然能够调用函数使用变量。

异步

异步原理

JavaScript是单线程语言,一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

var i, t = Date.now()
for (i = 0; i < 100000000; i++) {
}
console.log(Date.now() - t) 

从上面程序的执行结果可见,程序等待for循环执行完成才执行console.log。为了避免程序阻塞,JS对于这种场景设计了异步

如以下这段常见的ajax代码:

var ajax = $.ajax({
    url: '/data/data1.json',
    success: function () {
        console.log('success')
    }
})

和下面这段node.js代码:

var fs = require('fs')
fs.readFile('data1.json', (err, data) => {
    console.log(data.toString())
})

从上面两个 demo 看来,实现异步的最核心原理,就是将callback作为参数传递给异步执行函数,当有结果返回之后再触发 callback执行。

event-loop

异步编程的几种方式

回调

事件监听

发布订阅

生成器

Promise

async await

参考

[1] Javascript面试技巧(视频)
[2] 《JavaScript 忍者秘籍》
[3] 《你不知道的 JavaScript (下卷)》
[4] JavaScript中原型对象的彻底理解
[5] 最详尽的 JS 原型与原型链终极详解,没有「可能是」。

Comments
Write a Comment
  • fwind reply

    沙发!沙发!

  • App.Public reply

    沙发沙发沙发