闭包、作用域、原型小知识点等面试小知识点

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

 

事件委托(事件代理)是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托委托它们父级代为执行事件

当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。

 

递归调用

自己调用自己,称为递归调用

注意:下面这段代码运行会报错:Maximum call stack size exceeded

错误直译过来就是“栈溢出”,出现这个错误的原因是因为我进行了递归运算,但是忘记添加判断条件,导致递归无限循环下去

function fun()
{
    // 自己调用自己,称为递归调用
    fun();
    console.log("m2");
}
fun();

解决办法如下:

(function a(x) {
    // The following condition 
    // is the base case.
    if ( ! x) {
        return;
    }
    a(--x);
})(10);

 

事件委托

https://www.cnblogs.com/liugang-vip/p/5616484.html

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤,哈哈),比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。

Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题):

     window.onload = function(){
              var oUl = document.getElementById("ul1");
              oUl.onclick = function(ev){
                    var ev = ev || window.event;
                      var target = ev.target || ev.srcElement;
                    if(target.nodeName.toLowerCase() == 'li'){
                            alert(123);
                 alert(target.innerHTML);
                    }
              }
        }

https://www.cnblogs.com/liugang-vip/p/5616484.html

 

IE使用的是事件冒泡,其他浏览器是事件捕获 

IE提出的是冒泡流,而网景提出的是捕获流,后来在W3C组织的统一之下,JS支持了冒泡流和捕获流,但是目前低版本的IE浏览器还是只能支持冒泡流(IE6,IE7,IE8均只支持冒泡流),所以为了能够兼容更多的浏览器,建议大家使用冒泡流。

 

由此可以知道
  1、一个完整的JS事件流是从window开始,最后回到window的一个过程
  2、事件流被分为三个阶段(1~5)捕获过程、(5~6)目标过程、(6~10)冒泡过程

 

闭包就是能够读取其他函数内部变量的函数。

闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。这是优点也是缺点,不必要的闭包只会徒增内存消耗!

由于在javascript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。

所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁

闭包的使用:函数作为返回值,函数作为参数传递

// 函数作为返回值
function fn() { var max = 10; return function bar(x) {     if (x > max) {       console.log(x);     };   }; }; var abc = fn(); abc(12);

 

原型也是对象叫原型对象。

每个对象都有一个__proto__属性,指向创建该对象的函数的prototype

每个函数function都有一个prototype,即原型。每个对象都有一个__proto__,可成为隐式原型。

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

https://www.cnblogs.com/cxying93/p/6103375.html

http://www.cnblogs.com/wangfupeng1988/p/3977924.html    最全的理解JavaScript原型闭包等概念,一共16篇文章

 

一切(引用类型)都是对象,对象是属性的集合

 

对象都是通过函数来创建的

 

每个函数都有一个属性叫做prototype,这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。每个对象都有一个__proto__,可成为隐式原型,指向创建该对象的函数的prototype

 

对象的__proto__指向的是创建它的函数的prototype,就会出现:Object.__proto__ === Function.prototype

 

instanceof表示的就是一种继承关系,或者原型链的结构

 

访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链

 

那么我们在实际应用中如何区分一个属性到底是基本的还是从原型中找到的呢?大家可能都知道答案了——hasOwnProperty

 

所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”

 

在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。

 

在“准备工作”中完成了哪些工作:

  • 变量、函数表达式——变量声明,默认赋值为undefined;
  • this——赋值;
  • 函数声明——赋值;

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

 

函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

 

函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域

 

在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

 

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

其实,不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

this指向的是函数在运行时的上下文,既不是函数对象本身,也不是函数声明时所在作用域

 

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

其实这是一个压栈出栈的过程——执行上下文栈

 

javascript除了全局作用域之外,只有函数可以创建的作用域。

 

所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。

 

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

 

作用域在函数定义时就已经确定了。而不是在函数调用时确定。

 

作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。

 

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

 

如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值

 

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。

 

自由变量的取值:要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

 

闭包应用的两种情况即可——函数作为返回值,函数作为参数传递。

 

使用闭包会增加内容开销

 

多态意味着同名方法的实现依据类型有所改变,在JS中只需要在“子类”Student的prototype定义同名方法即可,因为原型链是单向的,不会影响上层的原型。

 

instanceof从字面意上来说就是判断当前对象是否是后面的实例, 实际上其作用是判断一个函数的原型是否在对象的原型链上

 

构造函数本身其实就是普通的函数,只是我们专门用它来实现了构造的功能,所以专门起了一个名字叫构造函数,任何函数都可以成为构造函数,这取决于你调用函数的方式,当使用了New的方式调用就成了构造函数。

 

原型:每个函数都有一个prototype属性,它是一个对象,也称作原型对象,我们可以把方法和属性写在它上面(不过原型对象不仅仅有我们写的属性和方法,还有别的,下面会介绍),而通过这个函数创建出来的实例对象,都能共享这个原型对象下的方法和属性。所以我们只需要把想要共享的东西放在函数的prototype下,不想共享的东西通过构造函数来创建就可以了。
 
每个实例化对象都有_proto_属性,它是一个指针,指向函数的prototype,也就是保存了它的地址。(JS中任何对象的值都是保存在堆内存中,我们声明的变量只是一个指针,保存了这个对象的实际地址,所以有了地址就能找到对象),
所以总得来说,每个实例化对象都有_proto_属性,保存了构造函数的原型对象的地址,通过这个属性就可以拥有原型对象下的所有属性和方法,_proto_属性实际就是实例化对象和原型对象之间的连接
 
继承:在不改变源程序的基础上进行扩充,原功能得以保存,并且对子程序进行扩展,避免重复代码编写
 
原生JS是弱类型语言,没有多态概念
 
构造函数的方法有一些规范:
1)函数名和实例化构造名相同且大写,(PS:非强制,但这么写有助于区分构造函数和
普通函数);
2)通过构造函数创建对象,必须使用new 运算符。
 
构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用new 运算符来调用,否则就是普通函数。
 

原型prototype解决了消耗内存问题。当然它也可以解决this作用域等问题。

我们经常把属性(一些在实例化对象时属性值改变的),定义在构造函数内;把公用的方法添加在原型上面,也就是混合方式构造对象(构造方法+原型方式)

 

我们把这个有__proto__串起来的直到Object.prototype.__proto__为null的链叫做原型链。如下图:

2dbd5870d84a471896d69f7d1980ae63

 

继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而ECMAScript 只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。

在JavaScript 里,被继承的函数称为超类型(父类,基类也行,其他语言叫法),继承的函数称为子类型(子类,派生类)

 

 

递归调用

函数的递归就是在函数中调用自身。使用递归函数一定要注意,处理不当就会进入死循环。递归函数只有在特定的情况下使用 ,比如阶乘问题

 

各种循环写法

       var arr = [2,4,6,8,10,12,14,16,18,20];
for (var i = 0; i < arr.length; i++) { console.log(arr[i]); } for(var i in arr){ console.log(arr[i]); } arr.forEach(function (val,index) { console.log(val); });
       // jquery写法 $.each(arr,
function(index,val) { console.log(val); });

 

数组去重

       Array.prototype.unique = function() {
                var result = [];
                this.forEach(function(v) {
                    if(result.indexOf(v) < 0) {
                        result.push(v);
                    }
                });
                return result;
            }

 

清除浮动

/* 1.添加新元素 */
<div class="outer">
  <div class="div1"></div>
  <div class="div2"></div>
  <div class="div3"></div>
  <div class="clearfix"></div>
</div>
.clearfix {
  clear: both;
}
/* 2.为父元素增加样式 */
.clearfix {
  overflow: auto;
  zoom: 1; // 处理兼容性
}
/* 3.:after 伪元素方法 (作用于父元素) */
.outer {
  zoom: 1;
  &:after {
    display: block;
    height: 0;
    clear: both;
    content: '.';
    visibillity: hidden;
  }
}

 

面试题

https://blog.csdn.net/SinceroTu/article/details/78055278

 

渐进增强(Progressive Enhancement):一开始就针对低版本浏览器进行构建页面,完成基本的功能,然后再针对高级浏览器进行效果、交互、追加功能达到更好的体验。

优雅降级(Graceful Degradation):一开始就构建站点的完整功能,然后针对浏览器测试和修复。比如一开始使用 CSS3 的特性构建了一个应用,然后逐步针对各大浏览器进行 hack 使其可以在低版本浏览器上正常浏览。

 

MVC

https://www.cnblogs.com/jinguangguo/p/3534422.html

我们让每一个层次去关注并做好一件事情,层与层之间保持松耦合,我们可以对每一个层次单独做好测试工作。如此,我们可以让代码更具可维护性。

使用MVC的设计思想,编写出高维护性的前端程序,主要目的是分离视图和模型

MVC是一种设计模式,它将应用划分为3个部分:数据(模型)、展现层(视图)和用户交互(控制器)。换句话说,一个事件的发生是这样的过程:
  1. 用户和应用产生交互。
  2. 控制器的事件处理器被触发。
  3. 控制器从模型中请求数据,并将其交给视图。
  4. 视图将数据呈现给用户。
我们不用类库或框架就可以实现这种MVC架构模式。关键是要将MVC的每部分按照职责进行划分,将代码清晰地分割为若干部分,并保持良好的解耦。这样可以对每个部分进行独立开发、测试和维护。

 

Vue

https://segmentfault.com/a/1190000006599500   剖析vue原理

https://www.cnblogs.com/libin-1/p/6893712.html      vue双向绑定原理

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js) 

数据劫持(vue.js)

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

 

Object.defineProperty

https://segmentfault.com/a/1190000011294519#articleHeader12

 

ES6

https://blog.csdn.net/qq_35480270/article/details/53978449

Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。

ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。第一种场景就是你现在看到的内层变量覆盖外层变量。而let则实际上为JavaScript新增了块级作用域。用它所声明的变量,只在let命令所在的代码块内有效。

另外一个var带来的不合理场景就是用来计数的循环变量泄露为全局变量

const有一个很好的应用场景,就是当我们引用第三方库的时声明的变量,用const来声明可以避免未来不小心重命名而导致出现bug

 

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        console.log(this.type + ' says ' + say)
    }
}
 
let animal = new Animal()
animal.says('hello') //animal says hello
 
class Cat extends Animal {
    constructor(){
        super()
        this.type = 'cat'
    }
}
 
let cat = new Cat()
cat.says('hello') //cat says hello

上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实力对象可以共享的。

Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Cat类,该类通过extends关键字,继承了Animal类的所有属性和方法。

super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

 

箭头函数:

它简化了函数的书写。操作符左边为输入的参数,而右边则是进行的操作以及返回的值Inputs=>outputs。

  • 不需要 function 关键字来创建函数
  • 省略 return 关键字
  • 继承当前上下文的 this 关键字
class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
 var animal = new Animal()
 animal.says('hi')  //animal says hi

当我们使用箭头函数时,函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它的this是继承外面的,因此内部的this就是外层代码块的this。

 

解构赋值:

数组和对象是JS中最常用也是最重要表示形式。为了简化提取信息,ES6新增了解构,这是将一个数据结构分解为更小的部分的过程

// 数组的解构赋值
let [name,age,sex] = ['牛牛',24,'女'] console.log(name + '--' + age + '---' + sex)

// 多层数组解构赋值
let [arr1,arr2,[arr3,arr4,[arr5,arr6]]] = [1,2,[3,4,[5,6]]];
console.log(arr1)
// 对象的解构赋值,注意名称必须和对象key一致
let {name,sex,age} = {name : '东东', sex : '男', age : 23}
console.log(name + '--' + age + '---' + sex)

//基本类型的解构赋值
let [a,b,c,d,e] = '我是中国人'
console.log(a)

 

展开运算符

ES6中另外一个好玩的特性就是Spread Operator 也是三个点儿...接下来就展示一下它的用途。

组装对象或者数组

   //数组
    const color = ['red', 'yellow']
    const colorful = [...color, 'green', 'pink']
    console.log(colorful) //[red, yellow, green, pink]
    
    //对象
    const alp = { fist: 'a', second: 'b'}
    const alphabets = { ...alp, third: 'c' }
    console.log(alphabets) //{ "fist": "a", "second": "b", "third": "c" }

 set、map数据集合

set集合和数组的区别就是数组可以有重复数据,但是set集合是没有重复数据的。

获取集合长度size属性

       let set = new Set(['张三','李四','王五']);
            
            // 集合长度
            console.log(set.size);
            
            // 添加
            set.add('牛雨晴');
            console.log(set);
            
            set.delete('张三');
            console.log(set);
            
            // has
            console.log(set.has('王五'))
            
            // 清空集合
            set.clear();
            console.log(set);

 

map也是没有重复数据的

map的话存放的是对象

       let obj1 = {'a':'1','b':'2'}
            let map = new Map([
                ['name','张三'],
                ['age',20],
                ['sex','男'],
                [obj1,'今天天气很好'],
                [[1,2,3],'适合敲代码']
            ]);
            console.log(map);
            console.log(map.size);
            
            // set方法
            map.set('friends','小花花')
            console.log(map)
            
            // get方法
            console.log(map.get('name'));
            
            // delete方法
            map.delete('name');
            console.log(map);
            
            // keys方法
            console.log(map.keys());
            
            // values方法
            console.log(map.values());
            
            // entries方法,得到键值对
            console.log(map.entries());
            
            // 对map进行遍历
            map.forEach(function (value,index) {
                console.log(index + ':' + value)
            })

 

symbol解决命名冲突问题

 

基于原型,通过构造函数实现面向对象

es6通过class实现面向对象

es6的class其实就是一个语法糖,底层还是基于原型和构造函数实现面向对象

 

模板字符串 ` ` 包裹

       // 模板字符串 ``包裹
            // 内容使用${}
            let str = 'hello world';
            let className = 'text';
            let html = `
                        <html>
                            <head></head>
                            <body>
                                <p class='${className}'>${str}</p>
                            </body>
                        </html>
                        
            `;
            console.log(html);

 

新增字符串方法

1.includes,是否包含某个字符串

2.startsWith,是否是以某个字母开头

console.log('hello'.includes('o'));

console.log('hello'.startsWith('o'));

 

异步执行可以用回调函数来实现。

 

Promise

https://www.jianshu.com/p/c98eb98bd00c

Promises是处理异步操作的一种模式,之前在很多三方库中有实现,比如jQuery的deferred 对象。当你发起一个异步请求,并绑定了.when(), .done()等事件处理程序时,其实就是在应用promise模式。

Promise是异步编程的一种解决方案,它有三种状态,分别是pending-进行中、resolved-已完成、rejected-已失败,相比传统回调函数更合理

所谓Promise ,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。 
Promise 对象的状态不受外界影响

古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。

 

postMessage(iframe间的跨域通信)

https://www.cnblogs.com/stephenykk/p/7193845.html

http://www.webhek.com/post/postmessage-cross-domain-post.html

HTML5给我们带来了安全的跨域通信接口,即window.postMessage()方法。

它方法原型是:window.postMessage(msg, domain),我们可以给指定的domain发送msg。而接收msg的iframe只要注册一个监听事件就可以了。

 

 

 

 

 

 

posted @ 2018-07-06 09:08  晴晴加油  阅读(996)  评论(0编辑  收藏  举报