6月29日得物一面
一、Object.prototype.toString() 的调用
对于 Object.prototype.toString()
方法,会返回一个形如 "[object XXX]"
的字符串。
如果对象的 toString()
方法未被重写,就会返回如上面形式的字符串。
({}).toString(); // => "[object Object]" Math.toString(); // => "[object Math]"
但是,大多数对象,toString()
方法都是重写了的,这时,需要用 call()
或 Reflect.apply()
等方法来调用。
var x = { toString() { return "X"; }, }; x.toString(); // => "X" Object.prototype.toString.call(x); // => "[object Object]" Reflect.apply(Object.prototype.toString, x, []); // => "[object Object]"
二、Object.prototype.toString()
的原理
对于 Object.prototype.toString.call(arg),若参数为 null 或 undefined,直接返回结果。
Object.prototype.toString.call(null); // => "[object Null]" Object.prototype.toString.call(undefined); // => "[object Undefined]"
若参数不为 null
或 undefined
,则将参数转为对象,再作判断。对于原始类型,转为对象的方法即装箱,此处不赘述。
转为对象后,取得该对象的 [Symbol.toStringTag]
属性值(可能会遍历原型链)作为 tag
,如无该属性,或该属性值不为字符串类型,则依下表取得 tag
, 然后返回 "[object " + tag + "]"
形式的字符串。
// Boolean 类型,tag 为 "Boolean" Object.prototype.toString.call(true); // => "[object Boolean]" // Number 类型,tag 为 "Number" Object.prototype.toString.call(1); // => "[object Boolean]" // String 类型,tag 为 "String" Object.prototype.toString.call(""); // => "[object String]" // Array 类型,tag 为 "String" Object.prototype.toString.call([]); // => "[object Array]" // Arguments 类型,tag 为 "Arguments" Object.prototype.toString.call((function() { return arguments; })()); // => "[object Arguments]" // Function 类型, tag 为 "Function" Object.prototype.toString.call(function(){}); // => "[object Function]" // Error 类型(包含子类型),tag 为 "Error" Object.prototype.toString.call(new Error()); // => "[object Error]" // RegExp 类型,tag 为 "RegExp" Object.prototype.toString.call(/\d+/); // => "[object RegExp]" // Date 类型,tag 为 "Date" Object.prototype.toString.call(new Date()); // => "[object Date]" // 其他类型,tag 为 "Object" Object.prototype.toString.call(new class {}); // => "[object Object]"
下面为部署了 Symbol.toStringTag
的例子。可以看出,属性值期望是一个字符串,否则会被忽略。
var o1 = { [Symbol.toStringTag]: "A" }; var o2 = { [Symbol.toStringTag]: null }; Object.prototype.toString.call(o1); // => "[object A]" Object.prototype.toString.call(o2); // => "[object Object]"
Symbol.toStringTag
也可以部署在原型链上:
class A {} A.prototype[Symbol.toStringTag] = "A"; Object.prototype.toString.call(new A()); // => "[object A]"
新标准引入了 [Symbol.toStringTag]
属性,是为了把此方法接口化,用于规范新引入的对象对此方法的调用。但对于“老旧”的对象,就只能直接输出值,以保证兼容性。
三、部署了此属性的内置对象
下面,我列出所有部署了此属性的内置对象。
- 三个容器对象。这类对象用作命名空间,用于存储同一类方法。
JSON[Symbol.toStringTag]; // => "JSON" Math[Symbol.toStringTag]; // => "Math" Atomics[Symbol.toStringTag]; // => "Atomic"
这三个对象的 toString()
都没有重写,直接调用 toString()
方法也可以得到相同的结果。
JSON.toString(); // => "[object JSON]" Math.toString(); // => "[object Math]" Atomics.toString(); // => "[object Atomics]"
2. 两个新引入的类型 BigInt
和 Symbol
。
BigInt.prototype[Symbol.toStringTag]; // => "BigInt" Symbol.prototype[Symbol.toStringTag]; // => "Symbol"
组件的封装
vue组件的定义
● 组件(Component)是Vue.js最强大的功能之一
● 组件可以扩展HTML元素,封装可重用代码
● 在较高层面上,组件是自定义元素,Vue.js的编译器为他添加特殊功能
● 有些情况下,组件也可以表现用 is 特性进行了扩展的原生的HTML元素
● 所有的Vue组件同时也都是Vue实例,所以可以接受相同的选项对象(除了一些根级特有的选项),并提供相同的生命周期钩子
vue组件的功能
1)能够把页面抽象成多个相对独立的模块
2)实现代码重用,提高开发效率和代码质量,使得代码易于维护
Vue组件封装过程
● 首先,使用Vue.extend()创建一个组件
● 然后,使用Vue.component()方法注册组件
● 接着,如果子组件需要数据,可以在props中接受定义
● 最后,子组件修改好数据之后,想把数据传递给父组件,可以使用emit()方法
组件使用流程详细介绍
1、组件创建---有3中方法,extend() <template id=''> <script type='text/x-template' id=''>
A、调用Vue.extend(),创建名为myCom的组件,template定义模板的标签,模板的内容需写在该标签下
var myCom = Vue.extend({ template: '<div>这是我的组件</div>' }) B、<template id='myCom'>标签创建,需要加上id属性 <template id="myCom"> <div>这是template标签构建的组件</div> </template> C、<script type='text/x-template' id='myCom'>,需加id属性,同时还得加type="text/x-template",加这个是为了告诉浏览器不执行编译里面的代码 <script type="text/x-template" id="myCom1"> <div>这是script标签构建的组件</div> </script>
2、注册组件----有2中方法,全局注册,局部注册
A1、全局注册:一次注册( 调用Vue.component( 组件名称,为组件创建时定义的变量 ) ),可在多个Vue实例中使用。
我们先用全局注册,注册上面例子中创建的myCom组件
Vue.component('my-com',myCom)
A2、全局注册语法糖:不需要创建直接注册的写法
Vue.component('my-com',{ 'template':'<div>这是我的组件</div>' })
'my-com'为给组件自定义的名字,在使用时会用到,后面myCom对应的就是上面构建的组件变量。
A3、如果是用template及script标签构建的组件,第二个参数就改为它们标签上的id值
Vue.component('my-com',{ template: '#myCom' })
B1、局部注册:只能在注册该组件的实例中使用,一处注册,一处使用
var app = new Vue({ el: '#app', components: { 'my-com': myCom } })
B2、局部注册语法糖:
var app = new Vue({ el: '#app', components: { 'my-com': { template: '<div>这是我的组件</div>' } } })
B3、<template>及<script>创建的组件,局部注册
var app = new Vue({ el: '#app', components: { 'my-com': { template: '#myCom' } } })
3、调用组件
只需要在调用组件的地方,写上组件名字的标签即可
<div> /*调用组件*/ <my-com></my-com> </div>
4、栗子
A、全局注册:新建一个html文件,引入vue.js,并且定义2个vue实例app1和app2
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue组件</title> <script src="vue.js"></script> </head> <body> <div id="app1"> <my-com></my-com> </div> <div id="app2"> <my-com></my-com> </div> <script> /*创建组件*/ var myCom = Vue.extend({ template: '<div>这是我的组件</div>' }); /*全局注册组件*/ Vue.component('my-com',myCom); /*定义vue实例app1*/ var app1 = new Vue({ el: '#app1' }); /*定义vue实例app2*/ var app2 = new Vue({ el: '#app2' }); </script> </body> </html>
显示效果:
可以看到,全局注册的组件在实例app1和实例app2中都可以被调用。
B、局部注册:将创建的组件注册到实例app1下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue组件</title> <script src="vue.js"></script> </head> <body> <div id="app1"> <my-com></my-com> </div> <div id="app2"> <my-com></my-com> </div> <script> var myCom = Vue.extend({ template: '<div>这是我的组件</div>' }); // Vue.component('my-com',myCom); /*局部注册组件*/ var app1 = new Vue({ el: '#app1', components:{ 'my-com':myCom } }); var app2 = new Vue({ el: '#app2' }); </script> </body> </html>
可以看到只渲染了app1实例下的组件,app2实例虽然调用了该组件,但是因为这个组件没有在其内部注册,也没有全局注册,所以报错说找不到该组件。
C、template 和script标签创建组件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue组件</title> <script src="vue.js"></script> </head> <body> <div id="app1"> <my-com></my-com> <my-com1></my-com1> </div> <template id="myCom"> <div>这是template标签构建的组件</div> </template> <script type="text/x-template" id="myCom1"> <div>这是script标签构建的组件</div> </script> <script> Vue.component('my-com1',{ //全局注册 template: '#myCom1' }); var app1 = new Vue({ el: '#app1', components:{ 'my-com':{ template: '#myCom' //局部注册 } } }); </script> </body> </html>
显示效果:
5、异步组件
vue作为一个轻量级前端框架,其核心就是组件化开发。我们一般常用的是用脚手架vue-cli来进行开发和管理,一个个组件即为一个个vue页面,这种叫单文件组件。我们在引用组件之时只需将组件页面引入,再注册即可使用。
当项目比较大型,结构比较复杂时,我们一般选用vue-cli脚手架去构建项目。因为vue-cli集成了webpack环境,使用单文件组件,开发更简单,易上手,尤其是在对组件的处理上。对于原生vue.js,我们就得将组件构建在同一个html的script标签下或者html的外部js中,所有组件集中在一块,不容易管理,这也是原生vue,js的一点不便之处
vue.js可以将异步组件定义为一个工厂函数。
使用$.get获取本地文件会跨域,所以要将项目部署到服务器中
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue组件</title> <script src="vue.js"></script> <script type="text/javascript" src='jquery-3.1.1.min.js'></script> </head> <body> <div id="app1"> <head-com></head-com> </div> <script> Vue.component('head-com', function (resolve, reject) { $.get("a.html").then(function (res) { resolve({ template: res }) }); }); var app1 = new Vue({ el: '#app1' }); </script> </body> </html>
显示效果如下:
6、Vue中的props数据流
通过在注册组件中申明需要使用的props,然后通过props中与模板中传入的对应的属性名,去取用这些值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue组件</title> <script src="vue.js"></script> <script type="text/javascript" src='jquery-3.1.1.min.js'></script> </head> <body> <div id='app'> <my-component name="jiangjiang" come-from="guilin"></my-component> <!-- 然后在模板中通过属性传值的方式进行数据的注入 --> </div> <script> Vue.component('my-component', { props: ['name', 'comeFrom'], //在注册组件的时候通过props选项声明了要取用的多个prop // 在注册组件的模板中使用到props选项中声明的值 template: '<p>我叫:{{name}}, 我来自:{{comeFrom}}</p>', created: function () { console.log('在created钩子函数中被调用') console.log('我叫:', this.name) console.log('我来自:', this.comeFrom) } }) new Vue({ el: '#app' }) </script> </body> </html>
注意:
A、props取值的方式
在注册组件的模板内部template,直接通过prop的名称取值就Ok
template: '<p>我叫:{{name}}, 我来自:{{comeFrom}}</p>'
不在注册组件的模板内部template,用this.prop的方式
console.log('我来自:', this.comeFrom)
B、在template选项属性中,可以写驼峰命名法,也可以写短横线命名法
在HTML(模板)中,只能写短横线命名法
原因:vue组件的模板可以放在两个地方
a、Vue组件的template选项属性中,作为模板字符串
b、放在.html中[ 用script template标签创建的组件 ],作为HTML
问题在于HTML不区分大小写,所以在vue注册组件中通用的驼峰命名法,不适用于HTML中的Vue模板,在HTML中写入props属性,必须写短横线命名法(把原来props属性中的每个prop大写换成小写,并且在前面加“-”)
将6中的
<div id='app'>
<my-component name="jiangjiang" come-from="guilin"></my-component>
<!-- 然后在模板中通过属性传值的方式进行数据的注入 -->
</div>
改成
<div id='app'>
<my-component name="jiangjiang" comeFrom="guilin"></my-component>
<!-- 然后在模板中通过属性传值的方式进行数据的注入 -->
</div>
显示效果,第二个没有显示
异步组件的实现原理;异步组件的3种实现方式---工厂函数、Promise、高级函数
异步组件实现的本质是2次渲染,先渲染成注释节点,当组件加载成功后,在通过forceRender重新渲染
高级异步组件可以通过简单的配置实现loading resolve reject timeout 4种状态
继承
1、原型链继承
核心: 将父类的实例作为子类的原型
function Parent1() { this.name = 'parent1'; this.play = [1, 2, 3] } function Child1() { this.type = 'child2'; } Child1.prototype = new Parent1(); console.log(new Child1()); // 潜在的问题 let s1 = new Child1(); let s2 = new Child1(); s1.play.push(4); console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4] // 两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。
特点:
1.非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2.父类新增原型方法/原型属性,子类都能访问到
3.简单,易于实现
缺点:
1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2.无法实现多继承
3.来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)
4.创建子类实例时,无法向父类构造函数传参
2、构造函数继承(借助 call)
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Parent1(){ this.name = 'parent1'; } Parent1.prototype.getName = function () { return this.name; } function Child1(){ Parent1.call(this); this.type = 'child1' } let child = new Child1(); console.log(child); // 没问题 console.log(child.getName()); // 会报错
特点:
1.解决了1中,子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类传递参数
3.可以实现多继承(call多个父类对象)
缺点:
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3、组合继承(前两种组合)
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } Parent3.prototype.getName = function () { return this.name; } function Child3() { // 第二次调用 Parent3() Parent3.call(this); this.type = 'child3'; } // 第一次调用 Parent3() Child3.prototype = new Parent3(); // 手动挂上构造器,指向自己的构造函数 Child3.prototype.constructor = Child3; var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play); // 不互相影响 console.log(s3.getName()); // 正常输出'parent3' console.log(s4.getName()); // 正常输出'parent3'
特点:
1.弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2.既是子类的实例,也是父类的实例
3.不存在引用属性共享问题
4.可传参
5.函数可复用
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4、原型式继承
ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)
let parent4 = { name: "parent4", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; let person4 = Object.create(parent4); person4.name = "tom"; person4.friends.push("jerry"); let person5 = Object.create(parent4); person5.friends.push("lucy"); console.log(person4.name); // tom console.log(person4.name === person4.getName()); // true console.log(person5.name); // parent4 console.log(person4.friends); // ['p1', 'p2', 'p3', 'jerry', 'lucy'] console.log(person5.friends); // ['p1', 'p2', 'p3', 'jerry', 'lucy']
通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法。
第一个结果“tom”,比较容易理解,person4 继承了 parent4 的 name 属性,但是在这个基础上又进行了自定义。
第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。
第三个结果“parent4”也比较容易理解,person5 继承了 parent4 的 name 属性,没有进行覆盖,因此输出父对象的属性。
最后两个输出结果是一样,其实 Object.create 方法是可以为一些对象实现浅拷贝的。
5、寄生式继承
使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。
let parent5 = { name: "parent5", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; function clone(original) { let clone = Object.create(original); clone.getFriends = function() { return this.friends }; return clone; } let person5 = clone(parent5); console.log(person5.getName()); // parent5 console.log(person5.getFriends()); // ['p1', 'p2', 'p3']
通过上面这段代码,我们可以看到 person5 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法。
6、寄生组合式继承
结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式。
function clone (parent, child) { // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程 child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; } function Parent6() { this.name = 'parent6'; this.play = [1, 2, 3]; } Parent6.prototype.getName = function () { return this.name; } function Child6() { Parent6.call(this); this.friends = 'child5'; } clone(Parent6, Child6); Child6.prototype.getFriends = function () { return this.friends; } let person6 = new Child6(); console.log(person6); // child6 {name: "parent6",play: [1, 2, 3], friends: "child5"} console.log(person6.getName()); // parent6 console.log(person6.getFriends()); // child5
通过这段代码可以看出来,这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销.
总体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式
ES6 提供了继承的关键字 extends
class Person { constructor(name) { this.name = name } // 原型方法 // 即 Person.prototype.getName = function() { } // 下面可以简写为 getName() {...} getName = function () { console.log('Person:', this.name) } } class Gamer extends Person { constructor(name, age) {
TS范式有哪些
一、泛型是什么?有什么作用
当我们定义一个变量不确定类型的时候有两种解决方式:
使用any
使用any定义时存在的问题:虽然 以 知道传入值的类型但是无法获取函数返回值的类型;另外也失去了ts类型保护的优势
使用泛型
泛型指的是在定义函数/接口/类型时,不预先指定具体的类型,而是在使用的时候在指定类型限制的一种特性。
二、泛型用法
2.1 在函数中使用泛型
function test <T> (arg:T):T{ console.log(arg); return arg; } test<number>(111);// 返回值是number类型的 111 test<string | boolean>('hahaha')//返回值是string类型的 hahaha test<string | boolean>(true);//返回值是布尔类型的 true
使用方式类似于函数传参,传什么数据类型,T就表示什么数据类型, 使用表示,T也可以换成任意字符串。
2.2 在接口中使用泛型
// 注意,这里写法是定义的方法哦 interface Search { <T,Y>(name:T,age:Y):T } let fn:Search = function <T, Y>(name: T, id:Y):T { console.log(name, id) return name; } fn('li',11);//编译器会自动识别传入的参数,将传入的参数的类型认为是泛型指定的类型
2.3 在类中使用泛型
class Animal<T> { name:T; constructor(name: T){ this.name = name; } action<T>(say:T) { console.log(say) } } let cat = new Animal('cat'); cat.action('mimi')
三、泛型约束
3.1使用接口约束泛型
interface Person { name:string; age:number; } function student<T extends Person>(arg:T):T { return arg; } student({name:'lili'});//类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性 student({ name: "lili" , age:'11'});//不能将类型“string”分配给类型“number” student({ name: "lili" , age:11});
3.2 数组泛型
let arr:Array<number> =[1,2,3] === let arr:number[]=[1,2,3]
四、泛型工具类型
4.1 Partial
partial<T>的作用就是将某个类型中的属性全部变为可选项?
示例:
interface Person { name:string; age:number; } function student<T extends Person>(arg: Partial<T>):Partial<T> { return arg; }
4.2 Record
Record<K extends keyof any, T>的作用是将K中所有的属性转换为T类型;示例:
interface PageInfo { title: string } type Page = 'home'|'about'|'other'; const x: Record<Page, PageInfo> = { home: { title: "xxx" }, about: { title: "aaa" }, other: { title: "ccc" }, };
4.3 Pick
Pick<T, K extends keyof T>的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型,示例:
interface Todo { title:string, desc:string, time:string } type TodoPreview = Pick<Todo, 'title'|'time'>; const todo: TodoPreview ={ title:'吃饭', time:'明天' }
4.4 Exclude
Exclude<T,U>的作用是将某个类型中属于另一个类型的属性移除掉,示例:
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" const t:T0 ='b';
4.5 ReturnType
returnType<T>的作用是用于获取函数T的返回类型,示例:
type T0 = ReturnType<() => string>; // string type T1 = ReturnType<(s: string) => void>; // void type T2 = ReturnType<<T>() => T>; // {} type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[] type T4 = ReturnType<any>; // any type T5 = ReturnType<never>; // any type T6 = ReturnType<string>; // Error type T7 = ReturnType<Function>; // Error
并行请求两个接口后再请求第三个
面试官问我,如何并行请求两个接口,拿到数据后再请求第三个接口?
完蛋,一时大脑一片空白,心想平时还真没遇到过类似场景。
最怕场面突然安静,寂静的几十秒中我惭愧的真想找个地缝钻进去。。。
一:Promise解决
对的,你没想错就是Promise的all方法。
我平时看的ES6关键时刻都到狗肚子去了,明明知道这个语法却联系不到实用场景上。
// 第一个请求 let p1 = new Promise((resolve) => { axios({ url: 'http://39.104.22.73:8081/ScoreRead/foreend', // 列表接口 method: 'post', }).then((resp) => { resolve(resp.data.data.num); }).catch(); }); // 第二个请求 let p2 = new Promise((resolve) => { axios({ url: 'http://39.104.22.73:8888/TagRead/foreend', // 标签列表接口 method: 'post', }).then((resp) => { resolve(resp.data.data.length); }).catch(); }); // 静态方法all的then方法就等同于,多个promise实例方法的多个then汇集 Promise.all([p1,p2]).then((value)=>{ console.log(value); $("#promise3").html(`${value[0]} + ${value[1]}`); // 这里可以再执行第三个请求 });
二:Axios的写法
function getUserAccount() { return axios.get('/user/12345'); } function getUserPermissions() { return axios.get('/user/12345/permissions'); } axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread(function (acct, perms) { // 两个请求现在都执行完成 }));