• 前端综合学习笔记---变量类型、原型链、作用域和闭包


    个人bolg地址

    变量类型

      JavaScript 是一种弱类型脚本语言,所谓弱类型指的是定义变量时,不需要什么类型,在程序运行过程中会自动判断类型。

    ECMAScript 中定义了 6 种原始类型:

    • Boolean
    • String
    • Number
    • Null
    • Undefined
    • Symbol(ES6 新定义)

    注意:原始类型不包含 Object。

    第一问:类型判断用到哪些方法?

    typeof

    typeof xxx得到的值有以下几种类型:undefined、boolean、number、string、object、functionsymbol ,比较简单。这里需要注意的有三点:

    1. typeof null结果是object ,实际这是typeof的一个bug,null是原始值,非引用类型
    2. typeof [1, 2]结果是object,结果中没有array这一项,引用类型除了function其他的全部都是object 
    3. typeof Symbol()typeof获取symbol类型的值得到的是symbol,这是 ES6 新增的知识点

    instanceof

    用于实例和构造函数的对应。例如判断一个变量是否是数组,使用typeof无法判断,但可以使用[1, 2] instanceof Array来判断。因为,[1, 2]是数组,它的构造函数就是Array。同理:

    function Foo(name) {
        this.name = name
    }
    var foo = new Foo('bar')
    console.log(foo instanceof Foo) // true

    第二问:值类型和引用类型的区别

      值类型 vs 引用类型

     除了原始类型,ES 还有引用类型,上文提到的typeof识别出来的类型中,只有objectfunction是引用类型,其他都是值类型。

    根据 JavaScript 中的变量类型传递方式,又分为值类型和引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的所有,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。

    面通过一个小题目,来看下两者的主要区别,以及实际开发中需要注意的地方。

    // 值类型
    var a = 10
    var b = a
    b = 20
    console.log(a)  // 10
    console.log(b)  // 20

    上述代码中,a b都是值类型,两者分别修改赋值,相互之间没有任何影响。再看引用类型的例子

    // 引用类型
    var a = {x: 10, y: 20}
    var b = a
    b.x = 100
    b.y = 200
    console.log(a)  // {x: 100, y: 200}
    console.log(b)  // {x: 100, y: 200}

    上述代码中,a b都是引用类型。在执行了b = a之后,修改b的属性值,a的也跟着变化。因为ab都是引用类型,指向了同一个内存地址,即两者引用的是同一个值,因此b修改属性时,a的值随之改动。

    再借助题目进一步讲解一下

    说出下面代码的执行结果,并分析其原因。

    function foo(a){
        a = a * 10;
    }
    function bar(b){
        b.value = 'new';
    }
    var a = 1;
    var b = {value: 'old'};
    foo(a);
    bar(b);
    console.log(a); // 1
    console.log(b); // value: new

    通过代码执行,会发现:

    • a的值没有发生改变
    • b的值发生了改变

    这就是因为Number类型的a是按值传递的,而Object类型的b是按共享传递的。

    JS 中这种设计的原因是:按值传递的类型,复制一份存入栈内存,这类类型一般不占用太多内存,而且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会因为不停复制内容而造成内存的浪费。

    引用类型经常会在代码中按照下面的写法使用,或者说容易不知不觉中造成错误!

    var obj = {
        a: 1,
        b: [1,2,3]
    }
    var a = obj.a
    var b = obj.b
    a = 2
    b.push(4)
    console.log(obj, a, b)//{a:1,b:[1,2,3,4]},2,[1,2,3,4]

    虽然obj本身是个引用类型的变量(对象),但是内部的ab一个是值类型一个是引用类型,a的赋值不会改变obj.a,但是b的操作却会反映到obj对象上。

    原型和原型链

    JavaScript 是基于原型的语言,原型理解起来非常简单,但却特别重要,下面还是通过题目来理解下JavaScript 的原型概念。

    第三问:如何理解 JavaScript 的原型

    对于这个问题,可以从下面这几个要点来理解和回答,下面几条必须记住并且理解

    • 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(null除外)
    • 所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象
    • 所有的的函数,都有一个prototype属性,属性值也是一个普通的对象
    • 所有的的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的prototype属性值

    通过代码解释一下,大家可自行运行以下代码,看结果

    // 要点一:自由扩展属性
    var obj = {}; obj.a = 100;
    var arr = []; arr.a = 100;
    function fn () {}
    fn.a = 100;
    
    // 要点二:__proto__
    console.log(obj.__proto__);
    console.log(arr.__proto__);
    console.log(fn.__proto__);
    
    // 要点三:函数有 prototype
    console.log(fn.prototype)
    
    // 要点四:引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
    console.log(obj.__proto__ === Object.prototype)

    原型

    先写一个简单的代码示例。

    // 构造函数
    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()
    

      执行printName时很好理解,但是执行alertName时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找,因此f.alertName就会找到Foo.prototype.alertName

    那么如何判断这个属性是不是对象本身的属性呢?使用hasOwnProperty,常用的地方是遍历一个对象的时候。

    var item
    for (item in f) {
        // 高级浏览器已经在 for in 中屏蔽了来自原型的属性,但是这里建议大家还是加上这个判断,保证程序的健壮性
        if (f.hasOwnProperty(item)) {
            console.log(item)
        }
    }
    

     第四问:如何理解 JS 的原型链

    还是接着上面的示例,如果执行f.toString()时,又发生了什么?

     

    // 省略 N 行
    
    // 测试
    f.printName()
    f.alertName()
    f.toString()

     

    因为f本身没有toString(),并且f.__proto__(即Foo.prototype)中也没有toString。这个问题还是得拿出刚才那句话——当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找。

    如果在f.__proto__中没有找到toString,那么就继续去f.__proto__.__proto__中寻找,因为f.__proto__就是一个普通的对象而已嘛!

    1. f.__proto__Foo.prototype,没有找到toString,继续往上找
    2. f.__proto__.__proto__即Foo.prototype.__proto__Foo.prototype就是一个普通的对象,因此Foo.prototype.__proto__就是Object.prototype,在这里可以找到toString
    3. 因此f.toString最终对应到了Object.prototype.toString

    这样一直往上找,你会发现是一个链式的结构,所以叫做“原型链”。如果一直找到最上层都没有找到,那么就宣告失败,返回undefined。最上层是什么 —— Object.prototype.__proto__ === null

    原型链中的this

     所有从原型或更高级原型中得到、执行的方法,其中的this在执行时,就指向了当前这个触发事件执行的对象。因此printNamealertName中的this都是f

    作用域和闭包

    作用域和闭包是前端面试中,最可能考查的知识点。例如下面的题目:

    第五问:现在有个 HTML 片段,要求编写代码,点击编号为几的链接就alert弹出其编号;

    <ul>
        <li>编号1,点击我请弹出1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>

    一般不知道这个题目用闭包的话,会写出下面的代码

    var list = document.getElementsByTagName('li');
    for (var i = 0; i < list.length; i++) {
        list[i].addEventListener('click', function(){
            alert(i + 1)
        }, true)
    }

    实际上执行才会发现始终弹出的是6,这时候就应该通过闭包来解决:

    var list = document.getElementsByTagName('li');
    
    for (var i = 0; i < list.length; i++) {
    
        list[i].addEventListener('click', function(i){
            return function(){
                alert(i + 1)
            }
        }(i), true)
    
    }

    要理解闭包,就需要我们从「执行上下文」开始讲起。

    执行上下文

    这个在我另一篇文章里讲过  点击链接

    先讲一个关于 变量提升 的知识点,面试中可能会遇见下面的问题,很多候选人都回答错误:

      第六问:说出下面执行的结果(这里我就直接注释输出了)

    console.log(a)  // undefined
    var a = 100
    
    fn('zhangsan')  // 'zhangsan' 20
    function fn(name) {
        age = 20
        console.log(name, age)
        var age
    }
    
    console.log(b); // 这里报错
    // Uncaught ReferenceError: b is not defined
    b = 100;

    在一段 JS 脚本(即一个<script>标签中)执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个 全局执行上下文 环境,先把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。再次强调,这是在代码执行之前才开始的工作。

    我们来看下上面的面试小题目,为什么aundefined,而b却报错了,实际 JS 在代码执行之前,要「全文解析」,发现var a,知道有个a的变量,存入了执行上下文,而b没有找到var关键字,这时候没有在执行上下文提前「占位」,所以代码执行的时候,提前报到的a是有记录的,只不过值暂时还没有赋值,即为undefined,而b在执行上下文没有找到,自然会报错(没有找到b的引用)。

    另外,一个函数在执行之前,也会创建一个 函数执行上下文 环境,跟 全局上下文 差不多,不过函数执行上下文 中会多出this arguments和函数的参数。参数和arguments好理解,这里的this咱们需要专门讲解。

    总结一下:

    • 范围:一段<script>js 文件或者一个函数
    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,thisarguments。

    this

    先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子:

    var a = {
        name: 'A',
        fn: function () {
            console.log(this.name)
        }
    }
    a.fn()  // this === a
    a.fn.call({name: 'B'})  // this === {name: 'B'}
    var fn1 = a.fn
    fn1()  // this === window

    this执行会有不同,主要集中在这几个场景中

    • 作为构造函数执行,构造函数中
    • 作为对象属性执行,上述代码中a.fn()
    • 作为普通函数执行,上述代码中fn1()
    • 用于call apply bind,上述代码中a.fn.call({name: 'B'})

    下面再来讲解下什么是作用域和作用域链,作用域链和作用域也是常考的题目。

    第七问:如何理解 JS 的作用域和作用域链

    作用域

    ES6 之前 JS 没有块级作用域。例如

    if (true) {
        var name = 'zhangsan'
    }
    console.log(name)

    从上面的例子可以体会到作用域的概念,作用域就是一个独立的地盘,让变量不会外泄、暴露出去。上面的name就被暴露出去了,因此,JS 没有块级作用域,只有全局作用域函数作用域

    var a = 100
    function fn() {
        var a = 200
        console.log('fn', a)
    }
    console.log('global', a)
    fn()

    全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样的坏处就是很容易撞车、冲突。

    // 张三写的代码中
    var data = {a: 100}
    
    // 李四写的代码中
    var data = {x: true}

    这就是为何 jQueryZepto 等库的源码,所有的代码都会放在(function(){....})())中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

    附:ES6 中开始加入了块级作用域,使用let定义变量即可,如下:

    if (true) {
        let name = 'zhangsan'
    }
    console.log(name)  // 报错,因为let定义的name是在if这个块级作用域

    作用域链

    首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量如何得到 —— 向父级作用域寻找。

    var a = 100
    function fn() {
        var b = 200
        console.log(a)
        console.log(b)
    }
    fn()

    如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。

    var a = 100
    function F1() {
        var b = 200
        function F2() {
            var c = 300
            console.log(a) // 自由变量,顺作用域链向父作用域找
            console.log(b) // 自由变量,顺作用域链向父作用域找
            console.log(c) // 本作用域的变量
        }
        F2()
    }
    F1()

    闭包

    讲完这些内容,我们再来看一个例子,通过例子来理解闭包。

    function F1() {
        var a = 100
        return function () {
            console.log(a)
        }
    }
    var f1 = F1()
    var a = 200
    f1()  //100

    自由变量将从作用域链中去寻找,但是 依据的是函数定义时的作用域链,而不是函数执行时,以上这个例子就是闭包。闭包主要有两个应用场景:

    • 函数作为返回值,上面的例子就是
    • 函数作为参数传递,看以下例子
    function F1() {
        var a = 100
        return function () {
            console.log(a)
        }
    }
    function F2(f1) {
        var a = 200
        console.log(f1())
    }
    var f1 = F1() F2(f1) //100

    至此,对应着「作用域和闭包」这部分一开始的点击弹出alert的代码再看闭包,就很好理解了。

  • 相关阅读:
    Linux中断的系统调用
    线程的概念
    C++ 虚函数表浅析
    C++虚函数工作原理
    深入浅出单实例Singleton设计模式
    《角斗士》一个帝国的史诗绝唱
    《妖猫传》大唐盛世背后那些事
    程序员之路
    职业规划
    一些重要的算法
  • 原文地址:https://www.cnblogs.com/yuanziwen/p/8531498.html
Copyright © 2020-2023  润新知