Vue重点知识总结—基础篇(一)

2018
05/10

1.Vue 中怎么自定义过滤器

可以用全局方法 Vue.filter() 注册一个自定义过滤器,它接收两个参数:过滤器 ID 和过滤器函数。过滤器函数以值为参数,返回转换后的值

Vue.filter('reverse', function (value) { 
    return value.split('').reverse().join('') 
})
<!-- 'abc' => 'cba' --> 
<span v-text="message | reverse"></span>

2.Vue 中怎么自定义指令

全局注册

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

局部注册

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

3.$route和$router的区别

$router 为 VueRouter 实例,想要导航到不同 URL,则使用 $router.push 方法$route 为当前 router 跳转对象里面可以获取 name 、 path 、 query 、 params 等

4.vue-router 使用params与query传参有什么区别

vue-router 可以通过 params 与 query 进行传参

// 传递
this.$router.push({path: './xxx', params: {xx:xxx}})
this.$router.push({path: './xxx', query: {xx:xxx}})

// 接收
this.$route.params
this.$route.query

params 是路由的一部分,必须要有。query 是拼接在 url 后面的参数,没有也可以。
params 不设置的时候,刷新页面或者返回参数会丢,query 则不会有这个问题。

5.vm.$mount()作用,$mount 和 el的区别

$mount和实例化Vue的el参数作用相同,是用来手动执行挂载到相应的dom元素。底层用的是querySelector()方法。若$mount不传参,则模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把实例的$el插入文档中(注意:当前vue实例或组件实例必须有template模板,否则$el为空),这样也可以渲染模板中的数据。

若使用script引入vue这种方式进行开发,非组件化开发,一般每个页面都会实例化一个新的vue实例。这时就不需要在实例中添加template模板,因为针对的是整个DOM。这时实例化时必须有el标签或调用$mount("dom")方法,且$mount方法必须传参,此时Vue会通过 el选项或$mount指定的挂载元素中提取出innerHtml作为其template模板编译渲染函数。否则数据或方法将无法挂载(vue不知道挂载到哪个元素上,使用appendChild挂载将无效,因为没有template)。

el和$mount区别

两者在使用效果上没有任何区别,都是为了将实例化后的vue挂载到指定的dom元素中。

如果在实例化vue的时候指定el,则该vue将会渲染在此el对应的dom中,反之,若没有指定el,则vue实例会处于一种“未挂载”的状态,此时可以通过$mount手动执行挂载

// 扩展组件
var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')

// 同上
new MyComponent({ el: '#app' })

// 实例化Vue
new Vue({ el: '#app' })

// 同上
new Vue({}).$mount('#app')

// 或者,在文档之外渲染并且随后挂载
// 注意:挂载必须使用appendChild方法,若使用innerHtml是无法挂载的,因为$el是一个dom对象,不是标签字符串
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

6.实例化Vue中的render参数作用(render:h=>h(App))

若使用script引入vue这种方式进行开发,实例化时并不需要render参数。因为没有组件template,此时Vue会通过 el选项或$mount指定的挂载元素中提取出innerHtml作为其template模板编译渲染函数。

若使用组件化开发,实例化vue时实际上需要一个外层template(比如app.vue),vue需要把这个模板先放到页面的某个标签内,再将数据挂载到这个标签。所以render方法就相当于组件中的templatecompoments两个参数的组合。只不过在组件实例中templatecompoments方式比render优先级高一些。而在vue实例中render优先级更高。

import Vue from 'vue';
import VueRouter from "vue-router";
import App from './App';
const app=new Vue({
    el:'#app',
    router,
    render:h => h(App) //App实际上就是一个组件
});

// 上述代码作用等同于
const app=new Vue({
    el:'#app',
    router,
    compoments:{
        App
    },
    template:"<App/>"
});

// 组件
var A = Vue.component("A",{
	template:"<div>{{notice}}</div>",
	data(){
		return{
			notice:"21212112"
		}
	}
})
const app=new Vue({
    el:'#app',
    render:h => h(A)
});

Vue 在创建 Vue 实例时,通过调用 render 方法来渲染实例的 DOM 树。Vue 在调用 render 方法时,会传入一个 createElement 函数作为参数,也就是这里的 h 的实参是 createElement 函数,然后 createElement 以App为参数进行调用,生成一个 VNode节点。render 函数得到这个VNode节点之后,返回给 Vue.jsmount 函数,渲染成真实 DOM 节点,并挂载到根节点上。

vue将createElement简化为h,因为它来自单词 hyperscript,这个单词通常用在虚拟dom的实现中。Hyperscript 本身是指生成HTML 结构的 script 脚本。

render:function(createElement){
    return creatElemnt(App);
}

7.Ajax请求放在哪个生命周期?

Ajax可以放到created及其之后的生命周期。

  1. created时候,DOM未挂载,这里请求的数据可以存到data中。但是无法操作dom,没办法将数据渲染到页面。
  2. mounted时DOM已挂载完成,这里可以操作DOM节点。一般情况下都放到mounted中。
  3. 生命周期时同步进行的,Ajax是异步进行的。为保证逻辑统一性,尽量将Ajax都放到同一个生命周期中。
  4. 服务端渲染时(SSR)不支持mounted(无DOM),所以放到created中。

8.何时需要使用beforeDestroy?

实例销毁之前钩子。在这里实例仍然可用。

  1. 可以解绑$on方法。使用$off方法。(在非父子组件通信时会借助中间Vue实例,然后使用vm.$emit()vm.$on()方法进行通信)
  2. 可以清除定时器。
  3. 可以移除绑定的事件监听。使用removeEventListener

9.Vue父子组件的生命周期调用顺序

渲染调用顺序:先父后子;渲染完成顺序:先子后父

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 若还有嵌套子组件,则将子mounted缓存,继续下级子组件渲染 -> 父mounted

更新操作:先父后子;更新完成顺序:先子后父

父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

销毁操作:先父后子;销毁完成顺序:先子后父

父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

10.vue组件如何通信

父子通信:子组件通过 props 属性,绑定父组件数据,实现双方通信

子父通信:由于Vue数据传递是单向数据流(儿子不能直接改父亲)

  1. 事件监听方法:父组件通过$on监听,子组件 $emit 触发。(简易的发布-订阅)
  2. 父组件通过props给子组件传递一个方法,子组件调用这个方法。

其他通信方式:

  1. 通过$children $parent获取父子组件的实例来拿到父子相应的数据。
  2. 通过provideinject在父组件中提供数据,子组件进行注入(写插件使用,把provide挂到$options上,内部会挂到实例上vm._provided,子组件会直接将其挂到数据中,而且是响应式的)。
  3. 通过ref获取组件的实例,从而拿到组件的数据。(普通标签获取的事dom元素,组件标签获取的是组件实例)

非兄弟组件通信:

  1. 在组件结构相对简单情况下,可以通过兄弟组件中的父组件, 子组件先把数据传递给父组件,  父组件再传递给另外的子组件。
  2. Event Bus 实现跨组件通信(公共的vue实例进行通信),如 Vue.prototype.$bus = new Vue()
  3. 使用状态管理插件vuex
//使用中间vue实例实现子-子通信

//mid
Vue.prototype.$bus = new Vue()

//某子组件发消息
//....
methods: {
    tellname () {
       // 发出事件,传递数据,givename自定义事件
     this.$bus.$emit('givename', this.mybfname) 
  } 
}

//某子组件收消息

// 组件一加载就进行兄弟组件所发出的事件的监听
created () {
    // vm.$on(vue自定义的事件, 处理函数)
    // 处理函数有一个默认参数,就是其它组件所传递的数据
    this.$bus.$on('givename', (data) => {
      console.log(data);
      this.mysbfname = data
    })
}

11.Vue相同逻辑如何抽离

Vue.mixin方法。可以给组件每个生命周期和函数混入一些公共逻辑。

12. Vue中如何实现⼦组件内的css样式名在项⽬中绝对唯⼀性

style标签上加上scoped属性

13. 说出⾄少4种Vue当中的指令和它的⽤法?

  1. v-if(判断是否隐藏,⽤来判断元素是否创建)
  2. v-show(元素的显⽰隐藏,类似css中的display的block和hidden)
  3. v-for(把数据遍历出来)
  4. v-bind(绑定属性、响应式数据)
  5. v-model(实现双向绑定、语法糖)

Vue重点知识总结—基础篇(二)

2018
05/10

1.Vue 组件 data 为什么必须是函数

因为js本身的特性带来的,Vue组件可能会有多个实例,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到当前组件所有实例的数据。如果将 data 作为一个工厂函数,那么每一个组件实例的 data 属性都是全新独立的,不会相互影响了,有效规避了很多组件状态污染问题。

通过查看源码发现,在state.js的initData方法中,对data的类型做了判断,如果是函数类型,则执行getData(data.call()直接执行)方法获取其值,如果是普通对象则返回原对象。

在组件中使用对象作为data的值,在编译时会直接报错。

原理:

  1. data是被挂到组件类的原型中,如果不是函数而直接是一个对象,会被其他任何组件继承,任何组件都可以对其进行修改。
  2. 如果data是一个工厂对象,每个组件进行调用时,执行函数都会返回一个全新的对象,就可以避免组件之间的数据互相影响。
  3. Vue实例化根组件时data是一个对象,是因为Vue根实例只会实例化一次,是单例的,实例不会被公用。不用考虑组件之间干扰问题。

2.计算属性computed 和 methods 有什么区别

我们可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的。

区别:

  1. computed计算属性是一个watcher有缓存,计算属性是基于依赖(依赖就是data中的数据)进行缓存的。只要依赖的数据不发生改变,计算属性中的方法是不需要执行的,一直使用缓存中的数据。当它的相关依赖发生改变时会重新赋值。当多次调用的时候只要依赖项不改变,就不会重新执行,从而节省了内存。(缓存机制在源码中通过dirty状态去判定)
  2. method 就是普通的函数。不依赖data中的数据,也没有缓存,每次调用都重新执行一遍。
  3. 函数的调用时需要加()的,而计算属性调用不需要加括号。
  4. 计算属性是必须有返回值的。

3.侦听器 watch和计算属性computed区别

两者的原理相同,在内部都是watcher,主要区别就是computed是有缓存的。如下:

  1. 侦听器 watch(数据一旦发生变化就通知侦听器所绑定的方法)。watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法;
  2. 计算属性computed是根据一个现有数据去生成一个新数据,并且这两个数据会永久的建立关系,还会建立缓存,当数据没有改变的时候,不会重新计算而是直接使用缓存中的值。
  3. 初始化computed计算属性时会实例化一个watcher,这个watcher有一个lazy:true标识,代表计算属性。还有一个默认dirty:true的标识,为true时才会让计算属性执行。当第一次取值时会执行,执行完毕后会将dirty改为false,也就是说下次再取值不会执行,会直接使用缓存的值。只有当计算属性所依赖的属性发生变化时,会将dirty改为true
  4. computed的底层还是使用Object.defindProperty来实现视图更新。
  5. watch一般用于异步、定时任务、或者开销较大(耗时)的操作。
  6. watch 中的属性 一定是data 中 已经存在的数据。
  7. 当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,此时就需要deep属性对对象进行深度监听(深度遍历和递归)。
  8. 使用 watch 选项允许我们执行异步操作,并在我们得到最终结果前,进行中间操作。也可以在数据频繁变化时进行自定义的操作如过滤操作。 而计算属性无法做到的。
  9. 计算属性是必须有返回值的。
watch:{
    obj:{ //监听的对象
        deep:true, //深度监听设置为 true
        handler:function(newV,oldV){
            console.log('watch中:',newV)
        }
    }
}
// deep:true的实现就是深度遍历和递归,很耗性能。
// 如果监听的是数组,deep:true会将数组中的每一项进行遍历取值,会将当前的watcher存到对应属性或数组项的依赖中,实现深层数据变化也会通知。

4.v-if 和 v-show 区别

使用了 v-if 控制页面元素节点是否渲染。v-show 改变 CSS 中的 display 显隐。故想要频繁控制显隐,最好使用v-show

两者不能连用,虽然连用不会报错,但是只有两者同时为true时才会显示节点,无法进行精准的判断。而且两者连用会在渲染函数中做两次判断,影响了性能。

底层原理:

  1. v-if在内部使用with语法,使用三元运算符判断v-if的值的真假。若为真,则渲染节点。若为假,则不渲染。
  2. v-show在内部使用创建指令方式,判断值为假,则改变节点样式display:none,若为真,则使用原有样式。

5.为什么v-if和v-for不能连用?

因为v-forv-if优先级高(vue2.x)。

通过测试得出,如果连用,在渲染函数($options.render)中v-for遍历出的每个元素中都添加v-if指令,性能很差。

所以一般在外层template标签添加v-if,这样如果为false,则不会执行v-for遍历,节省了性能开销。

优化:若非整体显隐,而是在列表中某些字内容有v-if的判断,这时也可以做一个计算属性,通过计算将不需要显示字内容进行过滤,节省了性能。

6.v-for中为什么要有key属性?

在Vue中使用v-for进行列表渲染的时候,它会默认使用“就地更新”的策略。直接对已有的标签就地更新,并不会将所有的标签全部重新删除和重建,只会重新渲染数据,然后再创建新的元素直到数据渲染完为止。

v-for渲染的列表更新时,diff算法会比较新旧节点的标签名key。若进行增加删除列表内节点后,key值会发生变化,那么新旧节点在进行比对时容易错位。最终导致渲染出错。

  1. key主要作用是为了高效得更新虚拟DOM。原理是VUE在patch过程中会通过key精准判断两个节点是否是同一个。从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
  2. 若不设置key,会在列表的渲染过程中发生一些隐蔽的bug
  3. 重复的 key 会造成渲染错误。

没有key情况还会导致节点的删除和重建,影响性能。若有key,在进行更新时会直接保留原有节点,只插入新节点,无需删除重建。

  1. v-for循环的时候,key属性只能使用number/string
  2. key属性值必须是唯一标识符。
  3. key属性不要使用索引作为标识。

用引索index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

7.vue的生命周期介绍

Vue实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载DOM->渲染、更新->渲染、卸载等一系列过程,我们称这是Vue的生命周期。

1.vue生命周期的作用是什么?

它的生命周期中有多个事件钩子,让我们在控制整个vue实例的过程时更容易形成好的逻辑。

在于Vue渲染过程的每个节点给一个通知(事件、回调),便于我们在某一个节点进行一些相关的操作。

2.Vue生命周期总共有几个阶段?

它可以总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/销毁后

3.第一次页面加载会触发那几个钩子?

第一次页面加载时会触发beforeCreate,created,beforeMount,mounted

4.DOM渲染在哪个周期中就已经完成?

DOM渲染在mounted中就已经完成了

5.生命周期的调用流程:

  1. beforeCreate:实例初始化完成,数据观测(data observer)之前。在这里拿不到实例中的数据。
  2. created:实例创建完成(数据观测完成、计算属性运算完成、method方法绑定、watch和event事件回调绑定),但dom还未挂载,数据还未渲染到页面,拿不到$el(拿不到dom元素)。<create 和 mount 的区别>
  3. beforeMounted:实例挂载之前执行。会调用render函数(调用template)。这个钩子基本用不到。
  4. mounted:实例挂载完成。创建新的vm.$el,把老的el对象删掉。这里可以拿到dom元素。
  5. beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
  6. updated:当前生命周期更新之后调用。由于数据更改,虚拟DOM渲染完成之后。
  7. beforeDestroy: 实例销毁之前调用。在这时,实例仍然可用。
  8. destroyed:实例销毁之后调用。调用后,实例所有数据监听都会解绑,所有事件监听都会移除,所有子实例也会被销毁。

6.生命周期钩子的一些使用方法(重点):

  1. beforeCreate:可以加个loading事件,在加载实例时候触发。
  2. created:在这里可以结束loading事件,执行异步请求将数据存如data。
  3. mounted:获取到dom节点,可以进行一些dom操作。由于这里能够取到dom,也可以在这里执行异步请求。
  4. beforeUpdate:在这里可以进一步更新状态,不会导致DOM重新渲染。
  5. updated:这里尽量不要再更新状态,否则可能会导致死循环。这里一般用不到。就算用到也是执行一些基于dom的操作,如动画效果。
  6. beforeDestroy:可以执行一些优化操作。如清空定时器,移除绑定事件等。
  7. nextTick:更新数据后立即操作dom,回调可以做一些在dom渲染完成后做的事。

8.父子组件执行顺序

在组件开始生成到结束生成的过程中,如果该组件还包含子组件,则自己开始生成后,要让所有的子组件也开始生成,然后自己就等着,直到所有的子组件生成完毕,自己再结束。所以上图中。“父亲”先开始自己的created,然后“儿子”开始自己的created和mounted,最后“父亲”再执行自己的mounted。

9.v-html指令会造成哪些问题?

1.可能会导致xss攻击。

2.会将标签内的子元素全部替换。

10.对 keep-alive 的了解

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存。缓存组件数据。组件切换时不会对其进行卸载。可以使被包含的组件保留状态,避免重新渲染。

keep-alive会改变组件的生命周期。当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。

  1. 有两个属性props:include和exclude,想缓存的内容和不想缓存的内容。max属性:最多缓存多少个。
  2. keep-alive内插入多个组件只会缓存第一个。

作用:①提升性能,避免每次重新渲染。(但也不宜太多,太多的话占用内存太大得不偿失)②保留状态(数据)

前端重点知识总结—BOM

2018
12/09

浏览器对象模型(Browser Object Model),不同浏览器提供了可以对浏览器窗口进行访问和操作的方法。

1.常用的Bom属性

① location对象

  1. location.href— 返回或设置当前文档的URL
  2. location.search — 返回URL中的查询字符串部分。例如 http://www.xxx.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu
  3. location.hash — 返回URL#后面的内容,如果没有#,返回空
  4. location.host — 返回URL中的域名部分,例如www.xxx.com
  5. location.hostname — 返回URL中的主域名部分,例如xxx.com
  6. location.pathname — 返回URL的域名后的部分。例如 http://www.xxx.com/xhtml/ 返回/xhtml/
  7. location.port — 返回URL中的端口部分。例如 http://www.xxx.com:8080/xhtml/ 返回8080
  8. location.protocol — 返回URL中的协议部分。例如 http://www.xxx.com:8080/xhtml/ 返回(//)前面的内容http:
  9. location.assign — 设置当前文档的URL
  10. location.replace() — 设置当前文档的URL,并且在history对象的地址列表中移除这个URL location.replace(url);
  11. location.reload() — 重载当前页面

② history对象

  1. history.go() — 前进或后退指定的页面数 history.go(num);
  2. history.back() — 后退一页
  3. history.forward() — 前进一页
  4. history.pushState(stateData,nameString,pathUrl) — 把路由页面放入历史记录,无刷新切换路由
  5. history.replaceState(stateData,nameString,pathUrl) — 不放入历史,直接切换

③ Navigator对象

  1. navigator.userAgent — 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
  2. navigator.cookieEnabled — 返回浏览器是否支持(启用)cookie

2.Cookie、sessionStorage、localStorage的区别

  1. Cookie:每次都会在同源的http请求头中携带(即使不需要,过多会带来性能问题),即cookie在浏览器和服务器间来回传递。而sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存。
  2. cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
  3. cookie存储的大小很小只有4K左右,而storage可以存5M甚至更大。
  4. sessionStorage仅在当前浏览器窗口关闭前有效,不可能持久保持;localStorage始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
  5. localStorage 和cookie在所有同源窗口中都是共享的。而sessionStorage在新标签页总是会初始化一个新的 session。
  6. localStorage可以用来在页面之间传递参数。
  7. 易用性:cookie需要程序员自己封装,原生的cookie使用url query形式存储,开发不友好。

3.浏览器在渲染页面时候会生成哪两棵树?

  1. DOM树:浏览器会遍历服务器返回的HTML文档节点,生成DOM树
  2. CSSOM规则树:浏览器解析CSS样式和文件生成。

4.浏览器缓存机制

浏览器的缓存分为强缓存协商缓存,当客户端请求某个资源的时候,获取缓存的流程如下:

  1. 先根据这个资源的http header(请求头)判断它是否命中强缓存,如果命中,则直接从本地缓存中获取资源,未命中则向服务器请求资源。
  2. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些请求头验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求而不返回资源,告诉客户端从缓存中获取,客户端收到返回后就直接从客户端缓存获取资源。协商缓存返回的状态码一般为304
  3. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求到服务器,但是协商缓存会发送请求到服务器
  4. 当协商缓存没有命中时,服务器会返回资源给客户端。
  5. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存
  6. 当F5刷新页面时,跳过强缓存但会检查协商缓存。
  7. Chrome在解析html时会对a标签的域名进行预解析,然后缓存。

强制刷新ctrl + f5 ,请求头header带有Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),同时不带有if-not-match / If-Modified-Since,所以请求服务器协商缓存会被当做失效,返回200和最新内容。

关于浏览器缓存的详细内容,包含请求头中携带的相关字段请点此查看

5.浏览器的内核分别是什么?

  1. IE: trident内核
  2. 火狐:gecko内核
  3. Safari:webkit内核
  4. Opera:以前是presto内核,现Blink内核(基于webkit)
  5. Chrome:Blink(基于webkit)

js知识总结–面向对象篇

2018
04/07

JavaScript 的核心是支持面向对象的,同时它也提供了强大灵活的 OOP 语言能力。面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象程序设计的目的是在编程中促进更好的灵活性和可维护性。

1.面向对象三要素

  1. 封装:一种把数据和相关的方法绑定在一起使用的方法。
  2. 继承:一个类可以继承另一个类的特征。如属性或方法。
  3. 多态:顾名思义「多种」「形态」。不同类可以定义相同的方法或属性。就像所有定义在原型属性内部的方法和属性一样,不同的类可以定义具有相同名称的方法;方法是作用于所在的类中。并且这仅在两个类不是父子关系时(两个类是同一个父类的不同子类)成立。

2.面向对象术语

  1. Namespace 命名空间允许开发人员在一个独特,应用相关的名字的名称下捆绑所有功能的容器。 在JavaScript中,命名空间只是另一个包含方法,属性,对象的对象。Javascript中的普通对象和命名空间在语言层面上没有区别。
  2. Class 类定义对象的特征。它是对象的属性和方法的模板定义。
  3. Object 对象类的一个实例。
  4. Property 属性对象的特征,比如颜色。
  5. Method 方法对象的能力,比如行走。
  6. Constructor 构造函数对象初始化的瞬间,被调用的方法。通常它的名字与包含它的类一致。

3.什么是构造函数,和普通函数的区别

函数:用来封装并执行代码块。

构造函数:一种特殊的函数,与关键字new一起使用,是用来生成实例对象的函数。作用是初始化对象, 即为对象赋初始值(添加属性和方法)。

区别:

  1. 构造函数是和new 关键字⼀起使⽤的
  2. 函数用来执行代码块,而构造函数用来创建对象
  3. 普通函数一般遵循小驼峰命名规则,构造函数一般遵循大驼峰命名规则
  4. 调用后返回内容不同,普通函数直接返回return定义的内容,没有return默认为undefined;而构造函数返回this指向的对象

4.什么是原型,什么是原型链?

原型:在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会与之关联一个原型对象。函数天生自带一个prototype属性(对象或构造函数实例自带__proto__属性),这个属性指向函数的原型对象。每一个对象或函数都会从原型中“继承”属性。(只有函数有prototype属性,而对象和数组本身没有。)

让我们用一张图表示构造函数和实例原型之间的关系:

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。

原型链:对象之间的继承关系,在JS中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条。由相互关联的原型组成的链状结构就是原型链

举例说明:person → Person → Object ,普通人继承人类,人类继承对象类

当我们访问对象的一个属性或方法时,它会先在对象自身(私有属性或方法)中寻找(判断是否为私有属性或方法而非继承来的,使用hasOwnProperty(key)方法),如果有则直接使用,如果没有则会去原型对象中寻找(构造函数的构造属性和方法优先级大于原型上属性和方法),如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则会找到null,返回undefined。null是原型链顶层。

我们可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性;使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true

function Person() {}
Person.prototype.a = 123;
Person.prototype.sayHello = function () {
    alert("hello");
};
var person = new Person()
console.log(person.a) //123
console.log(person.hasOwnProperty('a')) //false
console.log('a'in person) //true
console.log(person.__proto__===Person.prototype)   //true

person实例中没有a这个属性,从 person 对象中找不到 a 属性就会从 person 的原型也就是 person.__proto__,也就是 Person.prototype中查找,很幸运地得到a的值为123。那假如 person.__proto__中也没有该属性,又该如何查找?

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层Object为止。Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有__proto__这个属性。

console.log(Object.prototype.__proto__ === null) // true

5.构造函数的原型和实例化对象之间的关系

函数天生自带一个prototype属性,而这个属性指向一个对象(指向函数的原型对象),我们简称原型。

实例化对象会从构造函数的原型中继承其方法和属性,也就是继承prototype对象中的属性和方法。

实例化的对象是怎么指向(怎么继承)的prototype

实例化对象是通过__proto__属性继承的。这个属性指向构造函数的原型对象prototype。换句话说也就是__proto__可以访问prototype里面的所有属性和方法,__proto__prototype是同一个东西,其内存地址相同,__proto__ === prototype // true

prototype作用:

  1. 节约内存
  2. 扩展属性和方法
  3. 可以实现类之间的继承

prototype__proto__、实例化对象三者之间的关系:

  1. 每一个对象都有一个__proto__属性
  2. __proto__指向创建自己的那个构造函数的原型
  3. 对象可以直接访问自己__proto__里面的属性和方法
  4. constructor 指向创建自己的那个构造函数
({}).constructor === Object; // true  
Object.constructor === Function; // true

6.new关键字做了哪些事情(实例化对象或构造函数执行做了哪些事情)

new操作新建了一个对象(实例化构造函数)。这个对象原型链__proto__指向构造函数的prototype,执行构造函数后返回这个对象。构造函数被传入参数被调用,关键字this被指向该实例obj。返回实例obj。

  1. 在内存中开辟了一块空间
  2. 新生成了一个对象
  3. 链接到当前构造函数的原型
  4. 绑定 this,指向当前实例化的对象
  5. 返回这个新对象

隐式操作

开辟新的堆内存,构造函数的执行体this会指向这个堆。代码执行完毕后,会把这个堆内存地址返回并在栈中赋给新的变量。

构造函数执行前,会在函数体最前面添加一个 this = {},然后按照顺序执行代码,然后最后会有 return this; 其中 添加 this={} 以及 return this; 操作是隐式操作,js引擎会自动帮我们执行。

假如强行要改变构造函数返回的结果也是可以的,可以在构造函数里面添加return语句,但是除了{}[]、以及function类型会被如实返回,其他return的情况都会默认返回原来的构造函数的对象。

用函数方法手写一个new

function create() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

function aaa(a){
    this.a = a
}

new aaa(1) //aaa {a: 1}
create(aaa,1) //aaa {a:1}

function bbb(a){
    return {a:a}
}

new bbb(1) //{a: 1}
create(bbb,1) //{a:1}

7.原生js模拟ES6的类CLASS

//创建类-这个函数也相当于class中的constructor构造函数。
function Person(firstName) {
  this.firstName = firstName; //类的属性,可以用this关键字定义
  alert('Person instantiated');
}

//类的方法(原型链上创建)
Person.prototype.sayHello = function() {
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器,以便在实例化Student时能够执行Person的构造函数
  // 以便继承父类Person的属性
  Person.call(this, firstName);

  // 初始化Student类私有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
// 用于继承Person的原型链方法
// 这里最好不要直接Student.prototype = Person.prototype,
// 因为prototype是引用类型原因,一旦子类更改相应的方法,父类及其他子类都会继承。
Student.prototype = Object.create(Person.prototype); 
//Student.prototype = new Person(); //或者这样写

// 在不支持Object.create方法的浏览器中可以使用如下方法进行继承
// 这种方式叫做寄生类(中间类)方式,可以砍掉父类的构造函数,避免多次调用。
// 也可以避免直接使用Student.prototype  = Person.prototype造成的单一引用
function createObject(proto) {
    function ctor() { }
    ctor.prototype = proto;
    return new ctor();
}
// Usage:
Student.prototype = createObject(Person.prototype);

// 设置"constructor" 属性指向Student
// js 并不检测子类的 prototype.constructor,我们必须手动申明
// 若不指定,则constructor指向Person
Student.prototype.constructor = Student;

// 更换"sayHello" 方法,这里不会影响父类的sayHello方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

//实例化类,实例化时候构造函数会被立即执行
var person1 = new Person('Alice');//执行alert
var person2 = new Person('Bob'); //执行alert

//类的属性可以在实例化对象后访问
alert('person1 is ' + person1.firstName); // alerts "person1 is Alice"
alert('person2 is ' + person2.firstName); // alerts "person2 is Bob"

//调用类的方法
person1.sayHello(); // alerts "Hello, I'm Alice"
person2.sayHello(); // alerts "Hello, I'm Bob"

封装的体现:Student类虽然不需要知道Person类的walk()方法是如何实现的,但是仍然可以使用这个方法;Student类不需要明确地定义这个方法,除非我们想改变它。 这就叫做封装。

继承的体现:Student类作为 Person类的子类.可以继承父类的所有属性和方法。

多态的体现:我们重定义了sayHello() 方法并添加了 sayGoodBye() 方法.

8.构造函数里的方法和prototype上定义方法的区别

函数内的方法相当于是函数的私有变量,如果新创建的对象需要使用里面的私有变量,就使用函数内的方法。私有变量实现了面向对象的多态性。

在原型链上定义方法一般用于让所有实例和子类继承此方法。体现了面向对象的继承性。

区别:

  1. 定义在构造函数内部的方法,会在它的每一个实例上都克隆(重新定义)这个方法,消耗的内存更高。
  2. 定义在prototype 属性上的方法会让它的所有实例和子类都共享这个方法,不会重新复制。原型链上的方法指向一个内存引索,所有实例都是直接引用,更节省内存。
  3. 使用prototype的方法在继承的时候是共有的,多个实例或继承对象共有这个方法,所以一旦改变,所有的都改变。
  4. 在构造函数中定义的属性和方法要比原型prototype中定义的属性和方法优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的。
  5. 定义在构造函数中的属性会被序列化,而prototype中的属性不会被序列化。所以使用工厂模式定义对象的时候不能用原型prototype定义属性。

9.改变函数内部this指针的方法有哪些,区别是什么?

call,apply,bind三个方法。他们的第一个参数都是要改变的this指向。第二个参数是传给当前函数的参数。

这三个方法的作用是:①改变this指向;②借用别的对象的方法;③调用函数立即执行如call和apply。他们的区别如下:

  1. call:第二个参数是普通函数参数类型,可以有多个参数,使用call方法会立即执行当前函数。如Array.slice.call(this,arg1,arg2,arg3)
  2. apply:第二个参数是一个数组类型。apply只有两个参数。使用apply方法会立即执行当前函数。
  3. bind:第二个参数与call一致。bind方法不会立即执行当前函数,而是返回一个新的函数,需要再次调用(柯里化函数)。
  4. call()性能更佳。

手写call

Function.prototype.myCall = function(context){
	// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
	// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
	context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

	// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
	// 函数中的this指向调用它的对象
	context.fn = this
	const res = context.fn(...[...arguments].slice(1))

	//删除这个挂载的函数
	delete context.fn

	// 将执行结果返回
	return res
}

10.单继承和多继承

单继承:一个子类只有一个父类

多继承:一个子类可以有多个父类

11.构造函数的多种继承方式及其优缺点

先定义一个Animal类:

function Animal(name){
	// 属性
	this.name = name || "Animal"
	
	//实例方法
	this.sleep = function(){
		console.log(this.name + "在睡觉")
	}
}

// 原型方法
Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

① 原型链继承

方法:子类的原型指向父类的一个实例。这就是基于原型链的继承。

优点:

  1. 基于原型链,既是父类的实例,也是子类的实例。
  2. 可以继承父类的属性和构造方法,也能继承父类原型属性和方法。

缺点:无法实现多继承。

Animal.prototype.eat = function(food){
	console.log(this.name + "在吃" + food)
}

function Cat(){
	this.age = 10
}
Cat.prototype = new Animal()
Cat.prototype.name = "tom"
//以上两行也可以改为Cat.prototype = new Animal("tom")

// 测试
var cat = new Cat()
console.log(cat.name)	//tom
console.log(cat.age) //10
cat.eat("fish") //tom在吃fish
cat.sleep() //tom在睡觉
console.log(cat instanceof Animal) //true
console.log(cat instanceof Cat) //true
console.log(cat.constructor == Animal) //true 若不指定,则指向父类
console.log(cat.constructor == Cat) //false

// 指定构造函数
Cat.prototype.constructor = Cat
console.log(cat.constructor == Cat) //true

② 构造继承

方法:调用父类的构造函数来实现继承。相当于复制父类的构造属性和方法给子类。

优点:

  1. 可以实现多继承(继承多个父类的构造方法)
  2. 实例只是子类的实例,不是父类的实例
  3. 子类保留父类构造函数可传参的优势,如下例子传name

缺点:只能继承父类的构造属性和方法,无法继承父类的原型属性和方法。

function Dog(name){
	Animal.call(this,name) //调用父类构造方法,将this传与父类
        //这里可以调用多个父类,来实现多继承
}

// 测试
var dog = new Dog("wangwang")
console.log(dog.name)	//wangwang
dog.eat("rou")	//Uncaught TypeError: dog.eat is not a function
dog.sleep() //wangwang在睡觉
console.log(dog instanceof Animal) //false
console.log(dog instanceof Dog) //true
console.log(dog.constructor == Animal) //false
console.log(dog.constructor == Dog) //true

③ 组合继承

方法:即使用原型链继承和构造继承组合的方式实现继承。通过调用父类构造,继承父类的属性并保留传参的优点。然后通过将父类实例作为子类的原型,实现继承。

优点:

  1. 可以继承实例属性方法,也可以继承原型属性和方法。
  2. 可以实现多继承。

缺点:调用了两次父类函数,影响了性能,增加了内存负担。(在构造函数中调用了一次父类构造方法,在原型链继承时又初始化了一次父类)

function Cat(name){
	Animal.call(this,name);
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 测试
var cat = new Cat("tom")
console.log(cat.name)	//tom
cat.sleep()	//tom在睡觉
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
console.log(pig.constructor == Animal) //false 由于重新将Cat赋值给了constructor
console.log(pig.constructor == Cat) //true

④ 寄生类继承

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:由于原型链prototype是引用类型,无法直接将其赋值给子类的原型,会被子类直接修改。所以赋值给中间类,然后将中间类的实例赋给子类原型,避免了原型引用造成的原型开放(易被子类修改)。且直接赋值原型给寄生类,避免初始化父类而再次执行父类造成内存浪费。

缺点:只能继承父类的原型属性和方法,无法继承父类的构造属性和方法。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//创建寄生类实例
function superObj(proto) {
    function Super() {}
    Super.prototype = proto
    return new Super()
}

//将寄生类实例作为子类的原型
Pig.prototype = superObj(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

以上寄生类方法可以使用Object.create()方法取代。

function Pig(name){
	this.name = name //这里无法继承父类属性,需要重新定义
}

//将寄生类实例作为子类的原型
//Object.create()方法会创建一个新对象,类似于实例化的类
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //pig.sleep is not a function
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

⑤ 寄生组合继承-推荐

方法:通过寄生类(中间类)方式,实现原型继承。将父类的原型直接赋值给中间类,然后将中间类的实例赋给子类原型。

优点:

  1. 避免初始化父类两次造成内存浪费。
  2. 既能继承父类的原型属性和方法,也能继承父类的构造属性和方法。
  3. 可实现多继承。
function Pig(name){
	Animal.call(this,name); //调用父类,将其属性和方法拷贝给子类
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false

可在构造函数中调用多个父类,实现构造属性和方法的多继承。原型属性和方法多继承可以使用Object.assign(cur.prototype,otherFather.prototype)方法实现。

function Pig(name){
	Animal.call(this,name);
	Object1.call(this)
	Object2.call(this)
}

//将寄生类实例作为子类的原型
Pig.prototype = Object.create(Animal.prototype)

//Object.assign 会把其他原型上的属性和方法拷贝到Pig原型上
//只是原型方法的拷贝,而实例原型还是Animal
//若不支持Object.assign,则使用for.in遍历将对象合并
Object.assign(Pig.prototype,
	Object1.prototype,
	Object2.prototype,
	{a:1,b:2},
)

//测试
var pig = new Pig("佩奇")
console.log(pig.name)	//佩奇
pig.eat("剩饭")	//佩奇在吃剩饭
pig.sleep() //佩奇在睡觉
console.log(pig instanceof Animal) //true
console.log(pig instanceof Pig) //true
console.log(pig instanceof Object1) //false
console.log(pig.constructor == Animal) //true
console.log(pig.constructor == Pig) //false
console.log(pig.constructor == Object1) //false

12.this指向问题

  1. 默认绑定:全局环境中,this默认绑定到window。独立函数调用(直接调用,不使用a.b())都指向window。
  2. 隐式绑定:在对象字面量中,this隐式绑定到该对象。如:var obj = {a:1, fn:function(){ console.log(this.a) } }
  3. 隐式丢失:被隐式绑定的函数丢失绑定对象,从而this被绑到window。
  4. 显式绑定:使用call/apply/bind方法把对象绑定到this上
  5. new绑定:用new关键字实例化构造函数,this被绑到到实例化对象上。
  6. 函数中的this指向当前调用函数所处的环境,谁调用了它,this就指向谁。
  7. this指向和书写位置无关,和调用方式有关(this是在函数调用时入栈的执行上下文中动态绑定的)
  8. 优先级:new > 显式绑定(bind) > 隐式绑定 > 默认绑定

独立函数调用:

fn1(){
  console.log(this)  // window
}
fn2(){
  console.log(this)  // window
  fn1()
}
fn2()

// 以上都是window
var a = 1
function foo() {
	console.log(this.a)
}
foo()  //1
//函数中的this指向取决于谁调用了它
//foo() 相当于 window.foo(),所以指向window
//而var声明的变量会被挂载到window上

var obj = {
	a: 2,
	foo: foo
}
obj.foo() //2
//这里是obj调用了foo()所以this指向obj

// 以上两者情况 this只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,this`只会绑定在 c上,不会被任何方式修改 this指向
var c = new foo()
c.a = 3
console.log(c.a)  //3

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

构造函数默认不使用return关键字,他们通常初始化新对象。当构造函数函数体执行完毕后会显式返回这个对象。

箭头函数this指向于定义时所处的外层非箭头函数(注意这里是函数,若为对象则无效,再找外层)环境(取决于他外面的第一个不是箭头函数的函数this),一旦声明(绑定了上下文)无法改变。

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
a()()()  //window
//等同于window.a()()()
//这里的this指向函数a的this,而函数a的this在调用时是window调用的,所以指向window

//--------------------------------

var obj = {
	fn:() => {
		console.log(this)
	},
	fn2:function() {
		console.log(this)
	}
}
obj.fn() //window 由于箭头函数定义时外层是对象不是函数,那么再找外层,所以是window
obj.fn2() //obj

13.instanceof原理

instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。若找不到,会一直判断父类的prototype,直至Object

手写一个 instanceof

function _instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__  //这里如果找不到则找其原型的原型。比如数组最终原型指向Object
    }
}

_instanceof([],Array) //true
_instanceof([],Object) //true
[] instanceof Array //true
[] instanceof Object //true

前端重点知识总结—HTML/HTML5

2018
12/05

1. HTML5新增了哪些新特性?

新元素:

  1. 用于媒介的 video 和 audio 元素
  2. 语意化更好的内容元素,比如 article、footer、header、nav、section
  3. 新的表单控件,calendar、date、time、email、url、search、color
  4. 本地存储 localStorage和sessionStorage
  5. JS绘图 canvas

2.WebSocket的实现和应用

① 什么是WebSocket

WebSocket是html5中的协议,支持持久连接。http协议不支持持久连接。

② WebSocket特点

  1. 基于Http协议,或者说借用了http协议来完成一部分握手。在握手阶段与http是相同的。
  2. 可以取代 Ajax 轮询。
  3. 一个WebSocket握手协议的实现,基本有两个属性,告诉服务器发送的是WebSocket:upgrade:websocketconnection:Upgrade

② WebSocket使用

// 打开一个 web socket
var ws = new WebSocket("ws://localhost:9998/echo");

ws.onopen = function(){
  // Web Socket 已连接上,使用 send() 方法发送数据
  ws.send("发送数据");
  alert("数据发送中...");
};

ws.onmessage = function (evt) { 
  var received_msg = evt.data;
  alert("数据已接收...");
};

ws.onclose = function(){ 
  // 关闭 websocket
  alert("连接已关闭..."); 
};

3.HTML5拖放事件使用

拖放(Drag 和 drop)是 HTML5 标准的组成部分。

  1. ondragstart:事件主体是被拖放元素,在开始拖时触发
  2. ondarg:事件主体是被拖放元素,正在拖放时触发。
  3. ondragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
  4. ondragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
  5. ondragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
  6. ondrop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
  7. ondragend:事件主体是被拖放元素,在整个拖放操作结束时触发

例子

<!--拖动图片到矩形框中:-->
<style>
#div1{width:500px;height:300px;border:2px solid red;}
</style>
<div id="div1" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<!--draggable="true" 设置元素可被拖放-->
<img id="drag1" width="500" height="300" src="https://code.zuifengyun.com/wp-content/uploads/2021/06/d213d14979c83b01635bb8994fbe46b9f9e6ce84-700x280.jpeg" draggable="true" ondragstart="drag(event)">
<script>
    function allowDrop(ev){
	    ev.preventDefault();
    }
    function drag(ev){
	    ev.dataTransfer.setData("Text",ev.target.id);
    }
    function drop(ev){
	    ev.preventDefault();
	    var data=ev.dataTransfer.getData("Text");
	    ev.target.appendChild(document.getElementById(data));
    }
</script>

4.对HTML语义化标签的理解

HTML5语义化标签是指正确的标签包含了正确的内容,结构良好,便于阅读,比如nav表示导航条,类似的还有article、header、footer等等标签。

5.用一个div模拟textarea的实现

内容可编辑:给div添加contenteditable=true即可。

<style>
div{
    width: 400px;
    min-height: 100px;
    max-height: 300px;
     _height: 100px; //IE6
    margin-left: auto;
    margin-right: auto;
    padding: 3px;
    outline: 0;
    border: 1px solid #a0b3d6;
    font-size: 12px;
    word-wrap: break-word;
    overflow-x: hidden;
    overflow-y: auto; //超过最大高度就出现滚动条
    _overflow-y: visible;
}
</style>
<div contenteditable="true">
    .....此处省略.....
</div>

6.input与textarea的区别

<input>是单行文本框,不会换行。通过size属性指定显示字符的长度,注意:当使用css限定了宽高,那么size属性就不再起作用。value属性指定初始值,maxlength属性指定文本框可以输入的最长长度。可以通过width和height设置宽高,但是也不会增加行数。

<textarea>  是多行文本输入框,文本区中可容纳无限数量的文本,无value属性,不可通过value进行预设值,但可以使用value获取其值。其中的文本的默认字体是等宽字体(通常是 Courier) ,可以通过 cols 和 rows 属性来规定 textarea 的尺寸,不过更好的办法是使用 CSS 的 height 和 width 属性。

7.iframe有哪些缺点?

  1. iframe会阻塞主页面的onload事件
  2. iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
  3. iframe框架结构不好,如果嵌套多个iframe页面的货出现多个滚动条,用户体验差;
  4. iframe不利于搜索引擎优化;
  5. 很多移动设备无法完全显示框架,设备兼容性差;
  6. iframe框架页面会增加很多的http请求,增加服务器负担;
  7. 如果要使用最好是通过js动态给iframe添加src属性值,这样避免页面阻塞。

8.移动端适配之viewport

什么是viewport

<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />

手机浏览器是把页面放在一个虚拟的窗口-viewport中的,通常情况下这个虚拟的窗口比屏幕宽,这样就不用吧每个网页挤到很小的窗口中(这样会破坏没有针对手机浏览器优化的网页的布局),此时虽然显示不完全,但是用户仍可以通过平移和缩放来看网页的内容。viewport就是让网页开发者通过其大小,动态的设置其网页内容中控件元素的大小,从而使得在浏览器上实现和web网页中相同的效果(比例缩小)。

  1. width:控制viewport的大小,一般情况下指定为device-width(终端宽度,单位为缩放为100%的CSS像素),也可以指定一个固定的值例如600.
  2. height:和width相应,指定高度。
  3. initial-scal:初始缩放比例,页面第一次load的时候的缩放比例。
  4. maximum-scale:允许用户缩放到的最大比例。
  5. minimum-scale:允许用户缩放到的最小比例。
  6. user-scalable:用户是否可以手动缩放。

前端重点知识总结—HTTP

2018
06/05

1.http和https的基本概念

http: 超文本传输协议,是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

https: 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。https协议的主要作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。(简单来说https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。)https的SSL加密是在传输层实现的。

2.http和https的区别?

  1. Https协议需要ca证书,费用较高。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  3. 使用不同的链接方式,端口也不同,一般而言,http协议的端口为80,https的端口为443
  4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

3.https协议的工作原理

客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤:

  1. 客户使用https url访问服务器,则要求web 服务器建立ssl链接。
  2. web服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),返回或者说传输给客户端。
  3. 客户端和web服务器端开始协商SSL链接的安全等级,也就是加密等级。
  4. 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给服务端。
  5. web服务器通过自己的私钥解密出会话密钥。
  6. web服务器通过会话密钥加密与客户端之间的通信。

4.https协议的优点

  1. 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
  2. HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
  3. HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
  4. 谷歌搜索算法,采用HTTPS加密的网站在搜索结果中的排名将会更高。

5.https协议的缺点

  1. https握手阶段比较费时,会使页面加载时间延长50%,增加10%~20%的耗电。
  2. https缓存不如http高效,会增加数据开销。
  3. SSL证书也需要购买,功能越强大的证书费用越高。比如使用泛域名SSL证书。
  4. SSL证书需要绑定IP,不能再同一个ip上绑定多个域名,ipv4资源支持不了这种消耗。

6.tcp三次握手,一句话概括

客户端和服务端都需要知道各自是否可以进行收发,因此需要三次握手。相当于敲门,用于确认是否可以开始发送。

三次握手可以简化为:C发起请求连接,S确认,S也发起连接,C确认。我们再看看每次握手的作用:第一次握手:S只可以确认自己可以接受C发送的报文段;第二次握手:C可以确认S收到了自己发送的报文段;并且可以确认自己可以接受S发送的报文段;第三次握手:S可以确认C收到了自己发送的报文段。

7.说一下http2.0

简要概括:http2.0是基于1999年发布的http1.0之后的首次更新。

提升访问速度:请求资源所需时间更少,访问速度更快,相比http1.0。

多路复用:多路复用允许同时通过一个TCP连接发送多个请求。改善了:在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。

头部压缩:相同请求不需要再次发送请求头等信息,通信期间几乎不会改变的请求头的通用的键值,如user-Agent和content-Type只发送一次,相当于做了一层缓存

二进制分帧:HTTP2.0会将所有的传输信息分割为更小的信息或者帧,并对他们进行二进制编码。

8.400和401、403状态码

(1)400状态码:请求无效

产生原因:

前端提交数据的字段名称和字段类型与后台的实体没有保持一致

前端提交到后台的数据应该是json字符串类型,但是前端没有将对象JSON.stringify转化成字符串。

解决方法:

对照字段的名称,保持一致性

将obj对象通过JSON.stringify实现序列化

(2)401状态码:当前请求需要用户验证

(3)403状态码:服务器已经得到请求,但是拒绝执行

9.HTTP支持哪些请求方式

get、post、head、options、put、delete、trace、connect

10.HTTP请求的options,head方式

head:类似于get请求,只不过返回的响应中没有具体的内容,用户获取报头

options:允许客户端查看服务器的性能,比如说服务器支持的请求方式等等。

11.GET和POST的区别

  1. 副作用指对服务器上的资源做改变,比如搜索是无副作用的,注册是副作用的。Get 多用于无副作用场景,Post 多用于副作用场景。
  2. get参数通过url传递,post放在request body(请求体)中。
  3. get请求连接长度是有限制的(浏览器或服务器限制,HTTP不限制),post没有。
  4. post比get更加安全。因为get参数直接暴露在url中,不能用来传递敏感信息。
  5. get只能进行url编码。而post可以通过 request body来传输比 Get 更多的数据,支持多种编码方式且不对数据类型限制,如application/x-www-form-urlencodedmultipart/form-dataapplication/jsontext/xml
  6. get参数会被浏览器主动缓存,保留在浏览器历史记录中,而post参数不会
  7. get和post本质都是tcp连接,但由于浏览器和服务器的限制导致传输有一些差异。
  8. get产生一个tcp数据包,post产生两个tcp数据包。get方式会把请求头和data数据一并发出,服务器响应200;post请求先发请求头,服务器响应100,再发请求体数据,服务器响应200。

12.浏览器缓存机制

详见:浏览器的缓存机制详解

13.前端http优化

  1. 减少http请求数(http/1):合并css,合并JS,合并图片。但这种优化方式在http2.0中无效。
  2. 减少DNS查询时间
  3. 使用CDN
  4. 利用浏览器缓存:传输轻量、细粒度的资源,以便独立缓存和并行传输。因为HTTP/2的多路复用和头部压缩特性。
  5. 最小化HTTP请求大小
  6. 最小化HTTP响应大小
  7. 减少不必要的重定向
  8. 服务端渲染:允许服务端主动发送额外的资源。类似于ssr首屏的处理。

http2.x多路复用

多路复用使得不同的请求共用一个TCP连接,允许多个资源并行下载,避免建立多个连接带来不必要的额外开销。它消除了HTTP/1.1中的线头阻塞问题。头部压缩进一步减少了多个HTTP请求的开销,因为每个请求开销都小于未压缩的等价HTTP/1.1请求。

PS:文件合并依然可以提高压缩率,但它带来了代价高昂的缓存失效。即使有一行CSS改变了,浏览器也会强制重新加载你 所有的 CSS声明。另外,不是所有页面都使用了合并后的CSS或JavaScript文件中的全部声明或函数。

14.列举HTTP状态码,及如何获取

在原生xhr请求完成可通过XMLHttpRequest的实例中的status属性来获取。

var xhr = new XMLHttpRequest()
// open方法的第三个参数是是否异步
xhr.open('GET', 'http://xxxx.com/api?a=1&b=2' ,true)
xhr.send(null)
xhr.onreadystatechange = () =>{
	if(xhr.readyState==4){
		console.log(xhr.status, xhr.responseText)
	}
}
  • 100,接受的请求正在处理,信息类状态码
  • 2xx(成功)表示成功处理了请求的状态码
  • 200(成功)服务器已成功处理了请求
  • 206,服务器成功处理了部分请求,比如做分段下载(head方法获取文件大小,请求头字段用Range)
  • 3xx(重定向)表示要完成请求,需要进一步操作。通常这些状态代码用来重定向。
  • 301,永久性重定向,表示资源已被分配了新的 URL
  • 302,临时性重定向,表示资源临时被分配了新的 URL
  • 303,表示资源存在另一个URL,用GET方法获取资源
  • 304,(未修改),http请求的缓存问题引起,服务端不会返回新内容
  • 307,临时重定向
  • 4xx,(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理
  • 400,(错误请求)服务器不理解请求的语法
  • 401,当前请求需要用户验证
  • 403,(禁止)服务器拒绝请求(服务器已经得到请求,但是拒绝执行)
  • 404,(未找到)服务器找不到请求网页
  • 5xx,(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。
  • 500,(服务器内部错误)服务器遇到错误,无法完成请求
  • 502,服务端负载比较高,导致连接超时
  • 503,表示服务器宕机,无法处理请求

15.TCP和UDP的区别(了解)

UDP:

  • 面向无连接,不需要三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
  • 支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
  • 面向报文
  • 不可靠性:通信都不需要建立连接,想发就发,也不会关心对方是否已经正确接收到数据了,这样的情况肯定不可靠。
  • 恒定速率:UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
  • 头部开销小,传输数据报文时是很高效的。
  • 没有缓存,不会备份数据。

TCP:

  • 面向连接的、可靠的、基于字节流的传输层通信协议。流就是指不间断的数据结构。
  • 三次握手:相当于敲门,告知和确认可以开始收发。防止出现失效的连接请求,从而产生错误。这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
  • 只能进行点对点的数据传输,不支持多播和广播传输方式。
  • 每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收
  • 拥塞控制:当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
  • 全双工通信:TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段
  • TCP 针对数据包丢失的情况,可以采用重传机制解决

16.Chrome浏览器HTTP最大请求并发数限制

同一域名下,同一GET请求的并发数是1,也就是说上一个请求结束,才会执行下一个请求,否则置入队列等待发送;

同一域名下,不同GET/POST请求的并发数量是6。当发送的请求数量达到6个,并且都没有得到响应时,后面的请求会置入队列等待发送。

CSS重点知识总结—响应式布局

2017
17/11

1.rem布局的优缺点

rem本质是等比缩放。子元素尺寸设置rem单位,通过更改html元素的字体大小,就可以让子元素实际大小发生变化。

缺点:

  1. pc页面来讲使用次数不多,一般移动端可能会用;全适配方案可以使用媒体查询@media
  2. 数据量大:所有的图片,盒子都需要我们去给一个准确的值,才能保证不同机型的适配

2.常用的响应式布局有哪些方法

首先,在网页代码的头部,需要加入加入一行viewport元标签。作用为:网页宽度默认等于屏幕宽度(width=device-width),原始缩放比例(initial-scale=1)为1.0,即网页初始大小占屏幕面积的100%。

<meta name="viewport" content="width=device-width, initial-scale=1" />

css样式响应式的一些方法:

  1. 使用一些响应式单位如:em,rem,vw,vh,vmin,vmax,ch (小程序rpx)
  2. 尺寸使用百分比。
  3. 使用@media媒体查询。
  4. 使用max-widthmax-width等限制元素尺寸。
  5. 使用flex弹性布局。

3.常用的响应式单位有哪些

  • % 父元素尺寸的百分比为当前元素尺寸
  • em 优先根据自身的字体大小,如果没有就向上寻找最近父元素有设置字体大小来换算的倍数尺寸
  • rem 根元素(html元素或:root元素)设置的字体大小的倍数尺寸
  • ch 相对于数字0的大小。 1ch 就是数字 0 的宽度
  • vw 相对于视口(浏览器可视区域)的宽度,视口宽度会被均分为100单位,则1vw等于视口宽度的1%
  • vh 相对于视口的高度,视口高度会被均分为100单位,则1vh等于视口高度的1%
  • vmin 选取vw和vh中最小的那个均分为100单位来换算
  • vmax 选取vw和vh中最大的那个均分为100单位来换算

4.媒体查询的使用方式有哪些

① 直接在css中编写

@media 类型 and (条件1) and (条件二){
    css样式
}
//例:
@media screen and (max-width:980px ) {
    body{
        background-color: red;
    }
}

② 使用@import导入

@import url("css/moxie.css") all and (max-width:980px);

③ 也是最常用的:使用link连接,media属性用于设置查询方式:

<link rel="stylesheet" media="mediatype and|not|only (media feature)" type="text/css" href="mystylesheet.css">

<!-- 例子 -->
<link rel="stylesheet" media="screen and (min-width: 600px) and (max-width: 800px)" type="text/css" href="style.css">

前端基础知识总结—理论篇

2017
12/10

1.重绘与重排

重排(也叫回流)reflow/Relayout

当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。

浏览器渲染页面:浏览器渲染是基于“流式布局”的模型,流实际就使我们常说的文档流。浏览器根据渲染树中每个渲染对象的信息,计算出各自渲染对象的几何信息(DOM对象的位置和尺寸大小),并将其安置在界面中的正确位置。

会引起重排的操作有

  1. 页面首次渲染。
  2. 浏览器窗口大小发生改变。
  3. 元素尺寸或位置发生改变。
  4. 元素内容变化(文字数量或图片大小等等)。
  5. 元素字体大小变化。
  6. 添加或者删除可见的DOM元素。
  7. 激活CSS伪类(例如::hover)。
  8. 设置style属性
  9. 查询某些属性或调用某些方法。

重绘repaint

当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此损耗较少。比如修改字体颜色、背景、边框类型、阴影、显隐等。

重排一定会重绘,重绘不一定有重排。

避免重排

  1. 避免设置大量的style属性,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow,所以最好是使用class属性。
  2. 实现元素的动画,它的position属性,最好是设为absoulte或fixed,这样不会影响其他元素的布局。
  3. 动画实现的速度的选择。比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。
  4. 不要使用table布局,因为table中某个元素旦触发了reflow,那么整个table的元素都会触发reflow。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
  5. 如果在一个局部方法中需要多次访问同一个dom,则先暂存它的引用,避免多次访问dom。
  6. 少用DOM集合(类数组)来遍历,因为集合遍历比数组遍历耗费更高。
  7. 用事件委托来减少事件处理器的数量。

2.HTML和DOM的区别

  1. html是一种文档类型、是一种结构化文档、是一种标记语言。
  2. dom是具有一些API的文档对象模型(对象)。提供了一系列的对页面文档访问和操作的api。
  3. dom还可以操作其他的结构化文档,比如XML DOM 定义了一套标准的针对 XML 文档的对象。

3. 网页从输入网址到渲染完成经历了哪些过程?

大致可以分为:

  1. 输入网址;
  2. 查询本地DNS缓存,如果有这条记录,则直接返回对应的IP。否则请求DNS服务器,并获取对应的服务器ip地址(返回给客户机,并缓存这条记录),如果做了CDN加速的话,会提供访问速度最快的 IP 地址;
  3. 浏览器通过IP与端口号与服务器建立TCP连接,请求数据。(会与服务器进行3次握手)
  4. 浏览器向web服务器发送http请求,请求方式如get/post/delete/put/option等。数据在进入服务端之前,可能还会先经过负责负载均衡的服务器或网关,它的作用就是将请求合理的分发到多台服务器或端口上或进行重定向。
  5. web服务器响应请求,并返回指定url的HTTP报文(或错误信息);
  6. 首先浏览器会判断返回的状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。
  7. 一次会话完成,关闭
  8. 浏览器解析源文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。
  9. 解码完成后浏览器开始渲染流程,生成DOM树,解析css和js,渲染页面,直至呈现完成;(具体的渲染过程参考下文)

一文吃透浏览器渲染基本原理

4.什么是同源策略?

两个页面地址中的协议、域名和端口号一致,则表示同源。

同源策略的限制:

  1. 存储在浏览器中的数据,如localStroage、Cooke不能通过脚本跨域访问
  2. 不能通过脚本操作不同域下的DOM
  3. 不能通过ajax请求不同域的数据
  4. 跨域请求不能携带cookie信息,需要配置响应头”Access-Control-Allow-Credentials”: true

为什么会有同源策略:

目的是为了安全,如果没有同源限制,在浏览器中的cookie等其他数据可以任意读取,不同域下的DOM任意操作,ajax任意请求其他网站的数据,包括隐私数据。

5. 什么是跨域,解决跨域所有方法

用ajax访问非同源的接口地址,或非同源的页面直接传递数据,就会存在跨域问题。

解决跨域的办法:

1.通过jsonp跨域

缺点:

只能使用Get请求

不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败

JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保

2.通过修改document.domain来进行跨域

缺点:只能跨子域

3.HTML5新引进的window.postMessage方法实现跨域传参

缺点:会阻断后续程序的执行。所以只能放到程序最后。

4.CORS跨域(常用),服务器返回数据时修改响应头,使浏览器认为是同源。

Access-Control-Allow-Origin: *  //该字段必须,表示接受任意域名的请求

Access-Control-Allow-Methods: POST, GET, OPTIONS    //必须,永续请求的方式

Access-Control-Allow-Headers: X-PINGOTHER, Content-Type  //可选,允许接受除了6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma外的其他字段。

Access-Control-Max-Age: 86400  //指定本次预检请求的有效期,单位为秒。一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin请求头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

Access-Control-Allow-Credentials: true  //允许浏览器发送cookie

详细信息如下文:

Access-Control-Allow-前后端设置跨域CORS详解

5.动态创建script,script标签不受同源策略的限制。具体没考究。

6.iframe中使用利用location.hash 跨域传值

7.web socket协议进行跨域,建立长连接可以跨域。服务器需要配置支持web socket协议。

8.使用网关做反向代理,借助中间层。如nginx反向代理,node中间层反向代理。

6.图片懒加载和预加载

预加载:提前加载图片,当用户需要时直接从缓存中渲染。预加载是为了避免图片的加载过慢造成较差的用户体验,但是会增加服务器前端的压力。

懒加载:当浏览器滚动到相应的位置时再渲染相应的图片。目的是优化前端压力,减少请求数或延迟请求。对服务器前端有一定缓解压力作用。但是若图片较大,则会带来较差的用户体验。

懒加载实现方法:

图片懒加载实现方式

7.简单解释cookie和session

  1. cookie数据存放在本地浏览器,session数据存放在服务器。
  2. cookie并不是很安全,别人可以分析和修改cookie,考虑到安全赢个结合session使用。
  3. cookie结合session可以用来进行登陆或请求验证。session会保存到服务器上,有失效时间。cookie中保存session id
  4. 单个cookie数据不能超过4k,且很多浏览器限制了一个站点cookie的数量。比如20个。
  5. 由于安全性,现代ajax请求通常采用token方式取代cookie-session

8.异步加载js方式

① async属性

<script type="text/javascript" src="xxx.js" async="async"></script>
  1. async属性是HTML5新增属性,需要Chrome、FireFox、IE9+浏览器支持
  2. async属性规定一旦脚本可用,则会异步执行
  3. async属性仅适用于外部脚本
  4. 此方法不能保证脚本按顺序执行
  5. 他们将在onload事件之前完成

② defer属性

<script type="text/javascript" defer></script>
  1. defer属性规定是否对脚本执行进行延迟,直到页面加载为止
  2. 如果脚本不会改变文档的内容,可将defer属性加入到<script>标签中,以便加快处理文档的速度
  3. 兼容所有浏览器
  4. 此方法可以确保所有设置了defer属性的脚本按顺序执行
  5. 在IE中同时存在defer和async属性时,defer的优先级比较高

③ 异步创建script标签,插入到DOM

(function(){
    var scriptEle = document.createElement("script");
    scriptEle.type = "text/javasctipt";
    scriptEle.async = true;
    scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
    var x = document.getElementsByTagName("head")[0];
    x.insertBefore(scriptEle, x.firstChild);		
 })();

9. CDN是什么

CDN的全称是Content Delivery Network,即内容分发网络。其通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵跨运营商跨地域跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。 因为CDN的这些特性,我们可以将体积较大的文件或是图片上传到CDN中,通过CDN来加载,减轻了服务器的请求压力,同时也可以通过CDN来获取、加载依赖。

关于变量提升和作用域相关的题目

2017
22/06

1.变量提升,且提升的是申明,赋值不会提升

变量提升是js的预解析过程,在代码块执行前会进行隐性操作。

提升只针对var申明的变量和function具名函数。

console.log(a) // undefined,如果后面没有申明变量a,这里就不是undefined而是直接报错:is not defined
var a = 1
// 同理如下
var a1 = a2+1
var a2 = 10
console.log(a1) // NaN

// 上面代码等同于--------------------------------------

var a
var a1
var a2
console.log(a)  // undefined
a = 1
a1 = a2+1
a2 = 10
console.log(a1) // NaN

2.var会忽略后续同一个变量申明,只认可先前的申明

var a = 10
var a 
console.log(a) // 10
var a  = 5
console.log(a) // 5

// 上面代码等同于--------------------------------------

var a = 10
var a // 被忽略,相当于没有这一行
var a // 被忽略
console.log(a) // 10
a  = 5
console.log(a) // 5

// 上面代码等同于--------------------------------------

var a = 10
console.log(a) // 10
a  = 5
console.log(a) // 5

3.变量申明与赋值为`undefined`不同

// 赋值
var a = 'hello world'; 
var a = undefined;
console.log(a); // undefined

// 申明
var a = 'hello world'; 
var a;
console.log(a); // 'hello world‘

4.函数提升优先于变量提升,且全部提升

题目1

console.log(a) // ƒ a() {}
// 这就是为什么函数可以放到下面,在上面依然能调用的原因
// 函数会被提升,且优先于变量,且为全部提升,不只是申明
var a = 1
function a() {}
console.log(a) // 1  在这里a被重新赋值为1

// 上面代码等同于--------------------------------------

function a() {}
var a // 第二次申明被忽略
console.log(a) // ƒ a() {}
a = 1
console.log(a) // 1  在这里a被重新赋值为1

// 上面代码等同于--------------------------------------

function a() {}
console.log(a) // ƒ a() {}
a = 1
console.log(a) // 1  在这里a被重新赋值为1

题目2(重要)

fn() // 5
function fn(){ console.log(1) }
fn() // 5
function fn(){ console.log(2) }
fn() // 5
var fn = function(){ console.log(3) }
fn() // 3
function fn(){ console.log(4) }
fn() // 3
function fn(){ console.log(5) }
fn() // 3

// 上面代码等同于--------------------------------------

function fn(){ console.log(1) }
function fn(){ console.log(2) }
function fn(){ console.log(4) }
function fn(){ console.log(5) }
var fn 
fn() // 5
fn() // 5
fn() // 5
fn = function(){ console.log(3) }
fn() // 3
fn() // 3
fn() // 3

5.局部变量优先于全局变量

var a = 1;
fn();
function fn(){
    console.log(a); // undefined
    var a = 2
    console.log(a) // 2
}

// 第一个打印 undefined 是由于
// 函数申明也会被提升,且优先于变量
// 函数作用域中的变量的申明也会提升到函数最顶部
// 函数作用域中变量为局部变量,作用域链优先取局部值,局部没有才会找外层
// 上面代码等同于--------------------------------------

function fn(){
    var a
    console.log(a); // undefind 局部变量优先
    a = 2
    console.log(a) // 2
}
var a
a = 1
fn()

6.预编译在执行前进行,执行结果不影响预编译

var a = 1;
fn();
function fn(){
    console.log(a); // undefined
    return
    var a = 2
    console.log(a) // 不执行
}

// 上面代码等同于--------------------------------------

function fn(){
    var a
    console.log(a); // undefined
    return
    a = 2  // 不执行
    console.log(a) // 不执行
}
var a
a = 1;
fn();

7.函数作用域

function fn(){
   var aaa = 1
}
fn()
console.log(window.aaa) // undefined 对象上不存在的属性返回undefined
console.log(aaa) // 报错,未定义

注意:对象上找属性是原型链的查找,直接找变量是作用域链的查找。

8.局部找不到会到外层找,且函数在执行时变量才会查找

题目1

fn() // 在这里执行时候,a只是提升申明,还没有赋值
var a = 1;
function fn(){
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}

// 以上代码等同于----------------------------

function fn(){
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
var a
fn() // 在这里执行时候,a已经申明,还没有赋值
a = 1;

题目2

var a = 1;
fn() // 在这里执行,a已经被赋值
function fn(){
    console.log(a); // 1
    a = 2
    console.log(a) // 2
}

// 以上代码等同于----------------------------

function fn(){
    console.log(a); // 1
    a = 2
    console.log(a) // 2
}
var a
a = 1
fn() // 在这里执行,a已经被赋值

题目3(重要)

var n = 100
function fn(){
    n = 200
}
fn()
console.log(n) // 200

// 以上代码等同于----------------------------

function fn(){
    n = 200
}
var n
n = 100
fn()  // 在执行时,全局n为100,执行之后n被重新赋值为200
console.log(n) // 200

9.函数的作用域链外层要看函数申明位置,而不是在哪调用(重要)

函数执行时入栈开辟执行上下文,入栈位置时申明的位置。申明在全局执行上下文就在全局。

只不过时调用(执行)的时候才会去查找变量(第8条),局部没有就向外层查找。外层要看申明在哪里,而不是调用在哪里。

题目1(重要)

var a = 1;

function fn1(){
    console.log(a); // 1
}

function fn2(){
    var a = 2;
    console.log(a); // 2
    fn1()
}

fn2()
console.log(a) // 1

题目2(重要)

var a = 1;

function fn2(){
    function fn1(){
        console.log(a); // 2
    }
    var a = 2;
    console.log(a); // 2
    fn1()
}

fn2()
console.log(a) // 1

10.全局变量只要被申明,就会被挂到window上

只要申明,不需要赋值,就会挂到window

console.log('a' in window)  // false
console.log('sad' in window)  // true
var sad = 10
// in 可以判断对象自身的原型属性以及自身原型属性和方法(不可枚举的也能判断)

// 以上代码等同于----------------------------

var sad
// window.sad = sad  隐式操作
console.log('a' in window)  // false
console.log('sad' in window)  // true
sad = 10

11.var 不存在块级作用域

题目1

由于没有块级作用域,那么块级中的变量是全局变量,同样被提升

console.log('i:',i) // undefined
for(var i=0;i<3;i++){
    console.log("a:",i)
}
console.log("b:",i)
// i: undefined
// a: 0
// a: 1
// a: 2
// b: 3

// 以上代码等同于----------------------------

var i
console.log('i:',i) // undefined
i=0
for(;i<3;){
    console.log("a:",i)
    i++
}
console.log("b:",i)

// 以上代码等同于----------------------------

var i
console.log('i:',i) // undefined
i = 0
while(i<3){
    console.log("a:",i)
    i++
}
console.log("b:",i)

题目2(重要)

请注意,没有块级作用域,不管var变量在哪里申明,都会被提升。

变量提升是预编译的过程,预编译时候代码还未执行,不会有判断的操作。

console.log(a, b)  // undefined undefined
if(true){
  var a = 1
}else{
  var b = 2
}
console.log(a, b) // 1 undefined

// 以上代码等同于----------------------------

var a
var b
console.log(a, b)  // undefined undefined
if(true){
  a = 1
}else{
  b = 2
}
console.log(a, b) // 1 undefined

题目3(重要)

注意:函数在块{}中只提升申明,不提升整个函数

console.log(fn1)  // undefined
console.log(fn2)  // undefined
console.log('fn1' in window)  // true
// fn1()  // 报错:fn1 is not a function

{
    // 进了块中第一件事就是给fn1赋值
    fn1()  // 可运行

    function fn1(){
        console.log(1);
    }
}

if('fn2' in window){
    function fn2(){
        console.log(1);
    }
}

fn2() // 可运行

题目4

console.log(a) // undefined
if('a' in window){
  var a = 110
}
console.log(a)  // 110

// 以上代码等同于---------------------------

var a
// window.a = a 隐式操作
console.log(a) // undefined
if('a' in window){ // true
  a = 110
}
console.log(a)  // 110

12.没有加var的变量都是全局变量,不论在哪赋值,在任何位置都能访问

没有用关键字var赋值的变量会被隐性申明到全局。只有赋值之后才会被申明,不存在提升。

题目1

console.log(abc) // 报错 Uncaught ReferenceError: abc is not defined
fn()
function fn(){
 abc = 109
}

// 以上代码等同于----------------------------

function fn(){
 window.abc = 109
}
console.log(abc) // 在这里fn未执行, abc还未赋值
fn()

题目2

fn()
console.log(abc)  // 109
function fn(){
 abc = 109
}

// 以上代码等同于----------------------------

function fn(){
 window.abc = 109
}
fn()
console.log(abc)  // 这里已执行函数,abc 被赋值

题目3

fn()()
console.log(abc) // 109 
function fn(){
  return function(){
     abc = 109
  }
}
// 不管嵌套多少次都是挂到全局

13.var 连等,只有第一个变量是var申明且赋值,后面变量都是全局变量

题目1

console.log(aaa) // undefined
console.log(bbb) // 报错bbb is not defined
var aaa = bbb = ccc = 10
console.log(aaa, bbb, ccc) // 10, 10, 10

// 以上代码等同于----------------------------

console.log(aaa) // undefined
console.log(bbb) // 报错bbb is not defined
var aaa = 10
bbb = 10
ccc = 10
console.log(aaa, bbb, ccc)

// 以上代码等同于----------------------------

var aaa
console.log(aaa) // undefined
console.log(bbb) // 报错bbb is not defined
aaa = 10
bbb = 10
ccc = 10
console.log(aaa, bbb, ccc)

题目2

bbb = ccc = 10
console.log(bbb, ccc) // 10, 10

// 以上代码等同于----------------------------

bbb = 10
ccc = 10

题目3

function fn(){
    var aaa = bbb = ccc = 10 
    console.log(aaa, bbb, ccc) // 10, 10, 10
}
fn()
console.log(bbb, ccc) // 10, 10

// 以上代码等同于----------------------------

function fn(){
    var aaa =10 
    bbb = 10 // 挂到全局
    ccc = 10
    console.log(aaa, bbb, ccc) // 10, 10, 10
}
fn()
console.log(bbb, ccc) // 10, 10

题目4

var aaa = bbb = {a: 1, b: 2}
console.log(aaa, bbb) // {a: 1, b: 2}, {a: 1, b: 2}
console.log(aaa===bbb)  // true

// 等式后面先执行
// {a: 1, b: 2} 先在堆中开辟地址
// aaa 和 bbb 都指向这个地址

// 以上代码等同于----------------------------

0x666 = {a: 1, b: 2}  // 0x666 代表内存地址
var aaa = 0x666
bbb = 0x666 

14.关于形参和函数返回值(重要)

形参相当于函数内部申明的一个局部变量。

函数返回值默认是undefined。注意是需要执行过后。

var a = 12; b = 13; c = 14;  // 这里注意分号分开和逗号不同,逗号会全部使用var申明
function fn(a){
	console.log(a, b, c); // 10 13 14
	a = 100; // a已经被申明
	b = 200; // b在全局已经被申明
	console.log(a, b, c); // 100 200 14
}
b = fn(10); // 这里注意赋值给b的不是函数,而是函数执行后的返回值
console.log(a, b, c); // 12 undefined 14

// 以上代码等同于----------------------------

var a
function fn(x){
	var a = x
	console.log(a, b, c); // 10 13 14
	a = 100; // 在调用时a已经被申明,在函数作用域,赋值只修改了局部变量
	b = 200; // 在调用时b在全局已经被申明
	console.log(a, b, c); // 100 200 14
}
a = 12; 
b = 13; 
c = 14;
// 这里调用时函数内部的变量才会去查值
b = fn(10); // 这里注意赋值给b的不是函数,而是函数执行后的返回值
console.log(a, b, c); // 12 undefined 14

Access-Control-Allow-前后端设置跨域CORS详解

2017
15/04

跨域访问的项目常在过滤器或者拦截器中添加Access-Control-Allow-(访问权限控制,access-访问)前缀的响应头配置。

response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "accept,x-requested-with,Content-Type");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", "http://192.168.10.118:8070");

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

它允许浏览器向跨域(非同源)服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

本文详细介绍CORS的内部机制。

一、简介

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:

  1. HEAD
  2. GET
  3. POST

(2)HTTP的头信息不超出以下几种字段:

  1. Accept
  2. Accept-Language
  3. Content-Language
  4. Last-Event-ID
  5. Content-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

三、简单请求

3.1 基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

3.2 withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。(Credentials-资格/证书/凭证)

Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求

4.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

4.2 预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

4.3 浏览器的正常请求和回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是”预检”请求之后,浏览器的正常CORS请求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。