软件架构模式:
MVC 仅限于后端,前端只完成后端开发中view层。采用服务端渲染,采用观察者模式。
数据流混乱,若model层数据改变很难判断是model层直接更改还是view用户操作更改的,导致数据流混乱,相互依赖耦合度高。
分离两端联系,解决耦合度问题,但是presenter内容就多。
将数据变动在vm上处理,vm中利用diff算法,虚拟DOM等方法实现一套数据响应式机制自动适应model中的数据变化同时实现一套更新策略自动将数据变化转化为视图更新,减少大量代码,提高效率利于维护。
数据劫持+发布者和订阅者模式
// 实现vue双向数据绑定的原理
class Vue {
// 构造函数
constructor(obj_instance) {
this.$data = obj_instance.data;
Observer(this.$data)
Compile(obj_instance.el, this)
}
}
// 数据劫持 - 监听实例里的数据
function Observer(data_instance) {
// 递归出口,当属性没有子属性或者属性类型不是对象
if(!data_instance || typeof data_instance !== 'object') return;
// 实例化,避免多次添加订阅者
const dependency = new Dependency()
// 获取到key值,返回一个字符串数组
Object.keys(data_instance).forEach(key => {
// 首先获取到属性值
let value = data_instance[key]
// 递归子属性数据劫持
Observer(value)
// 操作对象, 操作属性, 相应操作
Object.defineProperty(data_instance, key, {
// 属性可以枚举
enumerable: true,
// 属性操作符可以被改变
configurable: true,
// 访问该属性会触发
get() {
// 订阅者加入依赖实例的数组
Dependency.temp && dependency.addSub(Dependency.temp)
return value
},
// 修改属性的时候会触发
set(newValue) {
value = newValue
Observer(newValue)
dependency.notify()
}
})
})
}
// HTML模板解析 - 替换DOM内
function Compile(element, vm){
// 返回文档中指定选择器,获取相应页面中的元素
vm.$el = document.querySelector(element)
// 创建一个虚拟的节点对象,用来创建文档碎片节点,可以包含各种类型的节点,在创建之处是空的。
const fragment = document.createDocumentFragment()
let child;
while(child = vm.$el.firstChild) {
fragment.append(child)
}
fragment_compile(fragment)
// 替换文档碎片内容
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if(node.nodeType === 3) {
// 提前保存,避免后面创建订阅者的时候更改的数据是更改过后的,并不是原始的值
const xxx = node.nodeValue
// 返回一个结果数组或null
const result_regex = pattern.exec(node.nodeValue)
if(result_regex) {
// 对字符串数组进行分割,以.分割,得到一个字符串数组。
const arr = result_regex[1].split('.')
// 直接得到value值
const value = arr.reduce(
(total, current) => total[current], vm.$data
)
node.nodeValue = xxx.replace(pattern, value)
// 创建订阅者,节点值替换内容的时候
new Watcher(vm, result_regex[1], newValue => {
node.nodeValue = xxx.replace(pattern, newValue)
})
}
return
}
if(node.nodeType === 1 && node.nodeName === 'INPUT') {
const attr = Array.from(node.attributes)
attr.forEach(i => {
if(i.nodeName === 'v-model'){
const value = i.nodeValue.split('.').reduce(
(total, current) => total[current], vm.$data
);
node.value = value
new Watcher(vm, i.nodeValue, newValue => {
node.value = newValue
})
node.addEventListener('input', e => {
const arr1 = i.nodeValue.split('.')
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce(
(total, current) => total[current], vm.$data
);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
node.childNodes.forEach(child => fragment_compile(child))
}
// 节点的子节点列表的末尾添加新的子节点
vm.$el.appendChild(fragment)
}
// 依赖 - 收集和通知订阅者
class Dependency {
constructor() {
this.subscribers = [];
}
addSub(sub) {
this.subscribers.push(sub)
}
notify() {
this.subscribers.forEach(sub => sub.update())
}
}
// 订阅者
class Watcher {
// vm表示vue实例, key表示实例的属性, 回调函数记录如何更新内容
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 临时属性 - 触发getter
// this指向构造函数的实例
Dependency.temp = this
key.split('.').reduce((total, current) => total[current], vm.$data)
Dependency.temp = null
}
update() {
const value = this.key.split('.').reduce(
(total, current) => total[current], vm.$data
)
this.callback(value)
}
}
new Vue(): 创建一个vue的实例对象。
data和methods中的数据还没有初始化。
要调用methods中的方法,或者操作data中的数据。
注意:beforeUpdate(),updateed()会根据data数据改变选择性触发0次或多次。
都是元素的显示和隐藏效果。
v-if:实际上是dom元素的创建或销毁。
v-show:相当与display: none/block。
async: 指明一个函数是异步函数, await 等待一个异步方法执行完成。
async函数返回一个promise对象。
promise三个状态:
pending:初始状态
fulfilled: 操作成功
rejected: 操作失败
改变原数组:
pop():删除数组的最后一个元素,且返回值为删除的元素的值,若数组为空,则返回undefined。
push(): 添加元素至数组中,修改的原数组, 返回新数组的新长度。
shift(): 删除数组的第一个元素,若数组为空,则该方法不进行任何操作,返回该元素的值。 --> 不接受参数,传入参数是没有意义的。
unshift(): 在数组头部添加元素,并且返回新数组长度。
reverse(): 对数组内容进行反转,改变原数组。
sort(): 对原数组进行排序。
splice(): 增加,替换和删除元素, 。 (index,howmany,item1…itemX)
concat(): 合并两个数组,返回一个新数组。
find(): 返回数组满足提供的测试函数的第一个元素的值,不满足返回undefined。
indexOf(): 找到一个给定元素的第一个索引,不存在就返回-1。
join(): 将数组所有元素连接成一个字符串并返回这个字符串。
slice(start, end - 表示数组下标): 一个含有被提取元素的新数组。
CSR: 浏览器渲染, 页面上的内容是加载JS文件渲染出来的,JS文件运行在浏览器上面,服务端只返回一个HTML文件。
SSR: 服务端渲染, 页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的HTML。
服务端渲染减少白屏时间还可以大幅度减少首屏加载时间。
从输入页面URL到页面渲染完成大致流程为: 本质就是两个IP通信。
解析URL
浏览器本地缓存
DNS解析
建立TCP/IP连接
发送HTTP请求
服务器处理请求并返回HTTP报文
浏览器根据深度遍历的方式把html节点遍历构建DOM树
遇到CSS外链,异步加载解析CSS,构建CSS规则树
遇到script标签,如果是普通JS标签则同步加载并执行,阻塞页面渲染,如果标签上有defer / async属性则异步加载JS资源
将dom树和CSS DOM树构造成render树
渲染render树
every() 数组内的所有元素是否通过某个指定函数的测试,返回一个布尔值。
filter() 创建一个新数组, 不会对空数组进行检测。
forEach() 对数组的每一个元素执行一次提供的函数。
some() 检测是否有一个元素可以通过被提供的函数方法,返回一个布尔值。
原型对象:
function Star(uname){
this.uname = this.uname
}
// Star.prototype = {
// // 指向构造函数,这里是赋值操作,没有指明构造函数,需要重新指向
// constructor: Star,
// sing: function(){
// console.log('111');
// },
// dance: function(){
// console.log('222');
// }
// }
// 此处是给原型对象添加一个函数。
Star.prototype.sing = function(){
console.log('333');
}
console.log(Star.prototype.constructor);
每一个构造函数都有一个prototype属性,最初是一个空对象,指向另一个对象,称为原型对象,对象中有一个constructor指向构造函数。
对象原型:
function Star(uname){
this.uname = this.uname
}
Star.prototype = {
// 指向构造函数,这里是赋值操作,没有指明构造函数,需要重新指向
constructor: Star,
sing: function(){
console.log('111');
},
dance: function(){
console.log('222');
}
}
// 此处是给原型对象添加一个函数。
// Star.prototype.sing = function(){
// console.log('333');
// }
const str = new Star()
// 实例对象上有一个__proto__属性指向原型对象。
console.log(str.__proto__);
实例对象的对象原型(proto)指向原型对象(prototype)。
对象原型中也有一个constructor属性,指向创建该实例对象的构造函数。
原型链:
每一个实例对象上都有proto属性,指向构造函数的原型对象,但是构造函数的原型对象也是一个对象其中也有proto属性,这样一层一层就形成了原型链。
function Star(uname){
this.uname = this.uname
}
Star.prototype = {
// 指向构造函数,这里是赋值操作,没有指明构造函数,需要重新指向
constructor: Star,
sing: function(){
console.log('111');
},
dance: function(){
console.log('222');
}
}
// 此处是给原型对象添加一个函数。
// Star.prototype.sing = function(){
// console.log('333');
// }
const str = new Star()
// 实例对象上有一个__proto__属性指向原型对象。
console.log(str.__proto__.constructor);
console.log(str instanceof Star); //true
console.log(str instanceof Object);//true
instanceof 检测构造函数prototype属性是否出现在某个实例对象的原型链上。
一个作用域可以访问另外一个函数内部的局部变量。
function demo() {
const num = 0;
function str(){
console.log(num);
}
return str;
}
const fn = demo()
fn() //输出0,访问到num属性
JS具有自动垃圾回收机制,不会造成内存泄漏(JS已经分配内存地址的对象由于长时间未进行内存释放或无法清除,造成长期占用内存,使得内存资源浪费。)
注意: 闭包会导致内存泄漏。
// 父类构造函数
function Person() {
this.name = 'ly';
this.eat = ['apple'];
// 若是箭头函数,this指向父级
this.getName = function(){
console.log(this.name);
}
}
Person.prototype.get = () => {
console.log("父类原型对象上的get方法");
}
// 子类构造函数
function Student() {}
// 子类 原型链继承 父类
Student.prototype = new Person()
// 子类实例化
const student = new Student()
// 通过原型链继承,若父类也有相同的基本数据类型变量,相当于给对象添加一个属性
// 若是相同的引用类型,则就是更改父类原型对象。
student.name = 'lyyy'
student.eat.push('pink'
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student.eat); //lyyy
console.log(student.name); //['apple', 'pink']
const student1 = new Student()
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student1.eat);//ly
console.log(student1.name);//['apple', 'pink']
缺点:
1. 父类所有的引用类型数据(对象,数组)会被子类共享,更改一个子类的数组,其他数据都会变化。
2. 子类实例不建议给父类构造函数传参, 若给父类传入一个引用类型的数据,若造成改变,那么其他子类都会收到影响。
// 父类构造函数
function Person() {
this.name = 'ly';
this.eat = ['apple'];
// 若是箭头函数,this指向父级
this.getName = function(){
console.log(this.name);
}
}
Person.prototype.get = () => {
console.log("父类原型对象上的get方法");
}
// 子类构造函数
function Student() {
Person.call(this)
}
// 子类实例化
const student = new Student()
// 通过原型链继承,若父类也有相同的基本数据类型变量,相当于给对象添加一个属性
// 若是相同的引用类型,则就是更改父类原型对象。
student.name = 'lyyy'
student.eat.push('pink')
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student.eat); //lyyy
console.log(student.name); //['apple', 'pink']
const student1 = new Student()
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student1.eat);//ly
console.log(student1.name);//['apple']
缺点:
子类不能访问父类原型属性(Person.prototype)上的方法和参数。
// 父类构造函数
function Person() {
this.name = 'ly';
this.eat = ['apple'];
// 若是箭头函数,this指向父级
this.getName = function(){
console.log(this.name);
}
}
Person.prototype.get = () => {
console.log("父类原型对象上的get方法");
}
// 子类构造函数
function Student() {
Person.call(this)
}
Student.prototype = new Person()
// 子类实例化
const student = new Student()
// 通过原型链继承,若父类也有相同的基本数据类型变量,相当于给对象添加一个属性
// 若是相同的引用类型,则就是更改父类原型对象。
student.name = 'lyyy'
student.eat.push('pink')
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student.eat); //lyyy
console.log(student.name); //['apple', 'pink']
student.get()
const student1 = new Student()
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student1.eat);//ly
console.log(student1.name);//['apple']
student.get()
优点:
1. 父类可以复用
2. 父类构造函数中的引用属性不会被共享
缺点: 会调用两次父类构造函数,会有两份一样的属性和方法,会影响性能。
// 父类构造函数
function Person() {
this.name = 'ly';
this.eat = ['apple'];
// 若是箭头函数,this指向父级
this.getName = function(){
console.log(this.name);
}
}
Person.prototype.get = () => {
console.log("父类原型对象上的get方法");
}
// 子类构造函数
function Student() {
Person.call(this)
}
// 利用一个第三方插入,相当于只是复制Person.prototype
const Fn = function(){}
Fn.prototype = Person.prototype
console.log(Person.prototype);
console.log(Fn);
// 实例化一个空对象
Student.prototype = new Fn()
console.log(Student.prototype);
// 子类实例化
const student = new Student()
student.name = 'lyyy'
student.eat.push('pink')
student.getName()
console.log(student.eat); //lyyy
console.log(student.name); //['apple', 'pink']
student.get()
const student1 = new Student()
student.getName()
console.log(student1.eat);//ly
console.log(student1.name);//['apple']
student.get()
优点: 避免调用两次父类构造函数
// 父类构造函数
class Person {
constructor(){
this.name = 'ly'
this.eat = ['apple']
this.getName = function(){
console.log(this.name);
}
}
get = () => {
console.log("父类原型对象上的get方法");
}
}
// 采用ES6继承
class Student extends Person {}
const student = new Student()
student.name = 'lyyy'
student.eat.push('pink')
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student.eat); //lyyy
console.log(student.name); //['apple', 'pink']
student.get()
const student1 = new Student()
// 若为箭头函数就是输出,ly,普通函数输出lyyy
student.getName()
console.log(student1.eat);//ly
console.log(student1.name);//['apple', 'pink']
student.get()
页面上的标签都是具体的虚拟dom对象,循环中,如果没有唯一的key,页面上删除一条标签,由于并不知道删除的是那一条,所以要把全部虚拟dom重新渲染,如果知道key为x标签被删除掉,只需要把渲染的dom为x的标签去掉即可。
每一个组件都是vue实例,组件共享data属性,当data的值是同一个引用类型的值时,改变其中一个就会影响其他的。
子绝父相 (知道元素高度, position: relative; position: absolute;)
transform (未知元素高度情况下实现元素垂直居中, translate() )
flex (justify-content: center; align-items: center)
CORS: 跨域资源共享。(服务器实现cors接口)
JSONP: 利用标签没有跨域限制的漏洞,网页可以得到从来源动态产生的JSON数据。JSONP请求一定需要对象的服务器做支持才可以。(只支持get请求,不安全可能会遭受XSS攻击)
与服务器交互:
cookie: 始终会在同源HTTP请求头中携带,在浏览器和服务器间来回传递。
sessionStorage, localStorage: 不会自动把数据发给服务器,仅在本地保存。
存储大小:
cookie 数据根据不同浏览器限制,一般不超过4K
sessionStorage和localStorage有5M升至更大
有效时间:
localStorage: 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
sessionStorage: 浏览器窗口关闭后自动删除
cookie:设置过期时间。
函数内部自己调用自己,可能会导致调用栈溢出。
网络优化:
- 使用缓存: 减轻服务端压力,快速得到数据。
- DNS预解析: link标签的ref属性设置dns-prefetch,提前获取域名对应的IP地址。
- 使用CDN:内容分发网络,将静态资源放在CDN上。
- 避免图片src为空:虽然src属性为空字符串,但浏览器依旧会向服务器发起一个HTTP请求。
页面渲染优化:
- 避免css选择器的复杂度: 减少嵌套
- 避免使用css表达式
- 使用外链式的js和css
- 使用字体图标iconfont代替图片图表
- 减少重绘和回流
JS中性能优化:
- 使用事件委托
- 防抖和节流
- 尽量不要使用JS动画
图片的优化:
- 精灵图
- 图片懒加载: 在图片即将进入可视区域的时候进行加载。
- 图片压缩
重绘: 某些元素外观被改变,比如元素的填充颜色
产生重绘:
- color
- visibility
- border-style
- text-decoration...
重排(回流): 重新生成布局,重新排列元素。比如元素的几何信息
产生重排:
- 页面初始化渲染
- 添加和删除可见的DOM元素
- 改变元素位置
- 改变元素尺寸,边距、填充、边框、宽度。。。
- 改变元素内容,文字数量、图片大小
- 改变元素字体大小
- 浏览器窗口尺寸。。。
优点:
- 节省监听数,节省内存。
- 可以监听动态元素。
阻止默认动作:
在a标签会跳转到href指定的网页,若想阻止该行为:
调用事件对象preventDefault()方法取消事件的默认操作。
浏览器从用户点击的按钮从下往上遍历至window,逐个触发事件处理函数。
如何阻止事件冒泡:
event.stopPropagation()
return false
vue实例最后会挂载在body标签里面,所以我们在vue中获取不了body标签的,如果要使用body标签的话需要用原生的方式获取。
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,若为基本类型,拷贝的就是基本类型的值,若是引用类型,拷贝的就是内存地址。
… 展开运算符
Object.assign() 合并对象,第二个参数的值和第一个参数对象的值合并,只改变第一个参数对象的值。
深拷贝:拷贝的只是对象,不是地址。
lodash 中 _.cloneDeep()
JSON.parse(JSON.stringify())
JSON.parse()是将JSON字符串转化为对象
JSON.stringify()将对象转化为JSON字符串
JS是一门单线程和非阻塞的脚本语言
事件循环: Event Loop
同步任务放入主线程执行栈中,异步任务放入事件队列中。当同步任务执行完毕(执行栈为空后),会从任务队列读取对应函数,放入主线程执行,以此循环。
异步任务分为 宏任务和微任务:微任务在宏任务执行之前。
宏任务:setInterval(), setTimeout()
微任务:promise.then(), Async/Await(实际上就是Promise), new MutaionObserver()
注意: promise的then()函数中才是异步任务,在promise中的执行语句是同步任务。
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
// 1 2 3 5 4
function unique(arr) {
return Array.from(new Set(arr))
}
// set返回一个对象,array.from()将对象转为数组。
function unique(arr) {
return arr.filter(function (item, index, arr){
// indexOf第二个参数表示开始要搜索的索引
return arr.indexOf(item, 0) === index
})
}
快排, 插入, 冒泡,归并…(去了解一下)
set()是返回一个对象,确保一个数组没有重复项。
map()返回一个满足回调参数(回调函数)的新数组。
清除浮动是为了解决,父元素(没有设置高度)因为子元素浮动引起的内部高度为0的问题。
添加无意义标签,语义化差
内容增多的时候容易造成不会自动换行导致内容被隐藏掉,无法显示要溢出的元素。
.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
content: "";
display: block;
height: 0;
clear:both;
visibility: hidden;
}
.clearfix{
*zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
}
"fahter clearfix">
"big">big
"small">small
"footer">
ie6-7不支持伪元素,使用zoom: 1触发。
.clearfix:after,.clearfix:before{
content: "";
display: table;
}
.clearfix:after{
clear: both;
}
.clearfix{
*zoom: 1;
}
"fahter clearfix">
"big">big
"small">small
"footer">
使用zoom:1触发。
浮动
绝对定位
flex布局
网格布局
通过img的data-src属性。 将src替换为data-src.
computed:
支持缓存,只有依赖数据发生变化,才会重新进行计算。
不支持异步。
监测的是依赖值。
watch:
不支持缓存。
支持异步操作。
监听某一个数据。
监听的函数接收两个参数,第一个参数是最新的值,第二个参数是输入之前的值。
父向子:props属性来传值,props只读。
子向父传值:$emit
兄弟组件:全局事件总线$bus
插槽
vuex是专为vue.js开发的状态管理模式,采用集中式存储管理应用的所有组件的状态,解决组件数据通信。
state:统一定义管理公共数据
mutations:用于修改数据
getters:vue的计算属性
actions:发送异步请求
modules:模块拆分
vue组件实例派发(dispatch)到actions中,通过异步请求获取数据,然后commit提交到mutations中,在mutations中修改数据。
typeof:只能检测出基本类型。(返回结果为number, Boolean, string, function, object, undefined)
instanceof:返回一个布尔值,由构造类型判断数据类型。
object.prototype.toString.call()判断类型
constructor:不能检测null和undefined。
标准盒子模型:width和height指的是内容区域的宽度和高度。
IE盒子模型中:width和height指的是内容区域+border+padding的宽度和高度。
promise是异步编程的一种解决方案。promise存在三种状态:pending,fulfiled,rejected。
解决地狱回调问题
支持多个并发请求,获取并发请求中的数据
promise解决异步问题。
promise构造函数接收一个参数:函数,这个函数需要传入两个参数(resolve, reject)
resolve:异步操作执行成功后的回调函数
reject:异步操作执行失败后的回调函数
箭头函数没有自己的this,this指向定义箭头函数时所处的外部执行环境的this。
即使调用call/apply/bind也无法改变箭头函数的this箭头函数本省没有名字,箭头函数不能new,会报错。
箭头函数没有arguments,在箭头函数内访问这个变量访问的是外部执行环境的arguments 箭头函数没有prototype。
post请求更安全(不会被缓存)
post发送的数据更大(请求数据在body中)
post(产生两个TCP包)比get(只产生一个TCP包)慢
post用于修改和写入数据,get用于搜索排序和筛选之类。
同域名,同协议,同端口
1xx 表示 HTTP 请求已经接受,继续处理请求
2xx 表示 HTTP 请求已经处理完成(200)
3xx 表示把请求访 问的 URL 重定向到其他目录(304 资源没有发生变化,会重定向到本地资源)
4xx 表示客户端出现错误 (403 禁止访问、404 资源不存在)
5xx 表示服务端出现错误
(块级格式化上下文)
创建了新的BFC的盒子是独立布局的,盒子内元素的布局不会影响盒子外面的元素,在同一个BFC中的两个相邻的盒子在垂直方向发生margin重叠的问题。
触发BFC:
display:flex
overflow: hidden
position: absolute
display: inline-block
基本数据类型:
布尔值(Boolean)
null
undefined
数字(number)
字符串(string)
symbol
bigInt
引用类型:
CSRF:跨站请求伪造
XSS:跨域脚本攻击
区别一:
CSRF :需要用户先登录网站 A ,获取 cookie
XSS :不需要登录。
区别二:(原理的区别)
CSRF :是利用网站 A 本身的漏洞,去请求网站 A 的 api 。
XSS :是向网站 A 注入 JS 代码,然后执行 JS 里的代码,篡改网站 A 的内容。
cookie 数据存放在客户的浏览器上,session 数据放在服务器上。
cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗
考虑到安全应当使用 session。
考虑到减轻服务器性能方面,应当使用 COOKIE。
单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。
所以个人建议:
将登陆信息等重要信息存放为 SESSION ;其他信息如果需要保留,可以放在 COOKIE 中
共同点 : 都可以改变 this 指向;
不同点: call 和 apply 会自动调用, 并且改变函数内部 this 指向. call 和 apply传递的参数不一样,call 传递参数使用逗号隔开,apply 使用数组传递 ,bind 不会调用函数,他会返回一个函数, 可以改变函 数内部 this 指向. 应用场景
call 经常做继承.
apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
bind 不调用函数,但是还想改变 this 指向. 比如改变定时器内部的 this 指向