前端面試題(JS部分)

內(nèi)置類型



JS中分為七種內(nèi)置類型确虱,其中內(nèi)置類型又分為兩大類型:

  • 基本類型
  • 對象(Object)

基本類型有六種:

  • null
  • undefined
  • string
  • number
  • boolean
  • symbol

其中JS的數(shù)字類型是浮點(diǎn)類型的含友,沒有整型。并且浮點(diǎn)類型基于 IEEE 754 標(biāo)準(zhǔn)實(shí)現(xiàn)校辩,在使用中會遇到某些 Bug窘问。NaN 也屬于 number 類型,并且 NaN 不等于自身宜咒。
對于基本類型來說惠赫,如果使用字面量的方式,那么這個變量只是個字面量故黑,只有在必要的時候才會轉(zhuǎn)換為對應(yīng)的類型儿咱。

let a = 111    // 這只是字面量庭砍,不是 number 類型
a.toString()   // 使用的時候才會轉(zhuǎn)換為對象類型

對象(Object)是引用類型,在使用過程中會遇到淺拷貝和深拷貝的問題概疆。

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name)    // EF

Typeof



typeof 對于基本類型逗威,除了 null 都可以顯示正確的類型。

typeof 1      // 'number'
typeof '1'    // 'string'
typeof undefined    // 'undefined'
typeof true    // 'boolean'
typeof Symbol()    // 'symbol'
typeof b    // b 沒有聲明岔冀,但是還是會顯示 undefined

typeof 對于對象凯旭,除了函數(shù)都會顯示 object。

typeof []    // 'object'
typeof {}    // 'object'
typeof console.log  // 'function'

對于 null 來說使套,雖然它是基本類型罐呼,但是會顯示 object,這是一個存在很久了的 Bug侦高。

typeof null // 'object'

類型轉(zhuǎn)換


轉(zhuǎn)Boolean

在條件判斷時嫉柴,除了 undefined、null奉呛、false计螺、NaN、''瞧壮、0登馒、-0 ,其他所有值都轉(zhuǎn)為 true 咆槽,包括所有對象陈轿。

對象轉(zhuǎn)基本類型

對象在轉(zhuǎn)換基本類型時,首先會調(diào)用 valueOf 然后調(diào)用 toString秦忿。并且這兩個方法是可以重寫的麦射。

let a = {
  valueOf() {
    return 0
  }
}

當(dāng)然也可以重寫 Symbol.toPrimitive ,該方法在轉(zhuǎn)基本類型時調(diào)用優(yōu)先級最高灯谣。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
1 + a    // => 3
'1' + a   // => '12'

四則運(yùn)算符

只有當(dāng)加法運(yùn)算時潜秋,其中一方是字符串類型,就會把另一個也轉(zhuǎn)為字符串類型胎许。其他運(yùn)算只要其中一方是數(shù)字峻呛,那么另一方就轉(zhuǎn)為數(shù)字。并且加法運(yùn)算會觸發(fā)三種類型轉(zhuǎn)換:將值轉(zhuǎn)換為原始值呐萨,轉(zhuǎn)換為數(shù)字杀饵,轉(zhuǎn)換為字符串莽囤。

1 + '1'    // '11'
2 * '2'    // 4
[1, 2] + [2, 1]    // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

對于加號需要注意這個表達(dá)式 'a' + + 'b'

'a' + +  'b'    // -> 'aNaN'
// 因?yàn)?+ 'b' -> NaN

== 操作符

比較運(yùn)算 x == y谬擦,其中 x 和 y 是值,產(chǎn)生 true 或者 false朽缎。這樣的比較按如下方式進(jìn)行:
1.若 Type(x) 與 Type(y) 相同惨远,則

  • 若 Type(x) 為 Undefined谜悟,返回 true。
  • 若 Type(x) 為Null北秽,返回 true葡幸。
  • 若 Type(x) 為 Number,則
    • 若 x 為NaN贺氓,返回 false蔚叨。
    • 若 y 為NaN,返回 false辙培。
    • 若 x 與 y 為相等數(shù)值蔑水,返回 true。
    • 若 x 為 +0 且 y 為 -0扬蕊,返回 true搀别。
    • 若 x 為 -0 且 y 為 +0,返回 true尾抑。
    • 其它情況返回 false歇父。
  • 若 Type(x) 為 String,則當(dāng) x 和 y 為完全相同的字符串序列(長度相等且相同字符在相同位置)時返回 true再愈。否則榜苫,返回 false。
  • 若 Type(x) 為 Boolean践磅,當(dāng) x 和 y 同為 true 或者 同為 false 時返回 true单刁。否則,返回 false府适。
  • 當(dāng) x 和 y 為引用同一對象時返回 ture羔飞。否則,返回 false檐春。

2.若 x 為 null 且 y 為 undefined逻淌,返回 true。
3.若 y 為 null 且 x 為 undefined疟暖,返回 true卡儒。
4.若 Type(x) 為 Number 且 Type(y) 為 String,返回 comparison x == ToNumber(y) 的結(jié)果俐巴。
5.若 Type(x) 為 String 且 Type(y) 為 Number骨望,返回比較 ToNumber(x) == y 的結(jié)果。
6.若 Type(x) 為 Boolean欣舵,返回比較 ToNumber(x) == y 的結(jié)果擎鸠。
7.若 Type(y) 為 Boolean,返回比較 x == ToNumber(y) 的結(jié)果缘圈。
8.若 Type(x) 為 String 或者 Number劣光,且 Type(y) 為 Object袜蚕,返回比較 x == ToPrimitive(y) 的結(jié)果。
9.若 Type(x) 為 Object 且 Type(y) 為 String 或 Number绢涡,返回比較 ToPrimitive(x) == y 的結(jié)果牲剃。
10.其他情況返回 false。
toPrimitive 就是對象轉(zhuǎn)基本類型雄可。

題目:
[ ] == ![ ] // -> true

// [] 轉(zhuǎn)成 true凿傅,然后取反變成 false
[] == false
// 根據(jù)第 7 條得出
[] == ToNumber(false)
[] == 0
// 根據(jù)第 9 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據(jù)第 6 條得出
0 == 0  // true

比較運(yùn)算符

  • 如果是對象,就通過 toPrimitive 轉(zhuǎn)換對象
  • 如果是字符串数苫,就通過 unicode 字符索引來比較

原型



示例圖.png

每個函數(shù)都有 prototype 屬性狭归,除了 Function.prototype.bind() ,該屬性指向原型文判。
每個對象都有 proto 屬性过椎,指向了創(chuàng)建該對象的構(gòu)造函數(shù)的原型。其實(shí)這個屬性指向了 [[prototype]] 戏仓,但是 [[prototype]] 是內(nèi)部屬性疚宇,我們并不能訪問到,所以使用 proto 來訪問赏殃。
對象可以通過 proto 來尋找不屬于該對象的屬性敷待,proto 將對象連接起來組成了原型鏈。

new



1.新生成了一個對象
2.鏈接到原型
3.綁定 this
4.返回新對象
在調(diào)用 new 的過程中會發(fā)生以上四件事情仁热,我們可以試著來自己實(shí)現(xiàn)一個 new

function create() {
  // 創(chuàng)建一個空的對象
  let obj = new Object()
  // 獲得構(gòu)造函數(shù)
  let Con = [].shift.call(arguments)
  // 鏈接到原型
  obj.__proto__ = Con.prototype
  // 綁定 this 榜揖,執(zhí)行構(gòu)造函數(shù)
  let result = Con.apply(obj, arguments)
  // 確保 new 出來的是個對象
  return typeof result === 'object' ? result : obj
}

對應(yīng)實(shí)例對象來說,都是通過 new 產(chǎn)生的抗蠢,無論是 function Foo() 還是 let a = { b: 1 }举哟。
對于創(chuàng)建一個對象來說,更推薦使用字面量的方式創(chuàng)建對象(無論性能上還是可讀性)迅矛。因?yàn)槟闶褂?new Object() 的方式創(chuàng)建對象需要作用域鏈一層層找到 Object妨猩,但是你使用字面量的方式就沒這個問題。

function Foo() {
  // function 就是個語法糖
  // 內(nèi)部等同于 new Function()
  let a = { b: 1 }
  // 這個字面量內(nèi)部也是使用了 new Object()
}

對于 new 來說秽褒,還需要注意運(yùn)算符優(yōu)先級壶硅。

function Foo() {
  return this;
}
Foo.getName = function () {
  console.log('1');
}
Foo.prototype.getName = function () {
  console.log('2');
}

new Foo.getName();    // 1
new Foo().getName();  // 2
// 等價于
new (Foo.getName());
new (Foo()).getName();

instanceof



instanceof 可以正確的判斷對象的類型,因?yàn)閮?nèi)部機(jī)制是通過判斷對象的原型鏈中是不是能找到類型的 prototype销斟。
我們可以試著實(shí)現(xiàn)一些 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__
  }
}

this



this 是很多人會混淆的概念庐椒,但是其實(shí)它一點(diǎn)也不難,你只需記住幾個規(guī)則就可以了蚂踊。

function foo() {
  console.log(this.a)
}
var a = 1
foo()

var obj = {
  a: 2,
  foo: foo
}
obj.foo()

// 以上兩者情況 `this` 只依賴于調(diào)用函數(shù)前的對象约谈,優(yōu)先級是第二個情況大于第一個情況

// 以下情況是優(yōu)先級最高的, `this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 還有種就是利用 call窗宇,apply,bind 改變 this特纤,這個優(yōu)先級僅次于 new

以上幾種情況明白了军俊,很多代碼中的 this 應(yīng)該就沒什么問題了,下面讓我們看看箭頭函數(shù)中的 this

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}
console.log(a()()())

箭頭函數(shù)其實(shí)是沒有 this 的捧存,這個函數(shù)中的 this 只取決于它外面的第一個不是箭頭函數(shù)的函數(shù)的 this粪躬。在這個例子中,因?yàn)檎{(diào)用 a 符合前面代碼中的第一個情況昔穴,所以 this 是 window镰官。并且 this 一旦綁定了上下文,就不會被任何代碼改變吗货。

執(zhí)行上下文



當(dāng)執(zhí)行JS代碼時泳唠,會產(chǎn)生三種執(zhí)行上下文

  • 全局執(zhí)行上下文
  • 函數(shù)執(zhí)行上下文
  • eval 執(zhí)行上下文

每個執(zhí)行上下文中都有三個重要的屬性

  • 變量對象(VO),包含變量宙搬、函數(shù)聲明和函數(shù)的形參笨腥,該屬性只能在全局上下文中訪問
  • 作用域鏈(JS 采用詞法作用域,也就是說變量的作用域是在定義時就決定了)
  • this
var a = 10;
function foo(i) {
  var b = 20;
}
foo();

對于上述代碼勇垛,執(zhí)行棧中有兩個上下文:全局上下文和函數(shù) foo 上下文脖母。

stack = [
  globalContext,
  fooContext
]

對于全局上下文來說尔邓,VO 大概是這樣的

globalContext.VO === globe
globalContext.VO = {
  a: undefined,
  foo: <Function>
}

對于函數(shù) foo 來說歉秫,VO 不能訪問,只能訪問到活動對象(AO)

fooContext.VO === foo.AO
fooContext.AO = {
  i: undefined,
  b: undefined,
  arguments: <>
}
// arguments 是函數(shù)獨(dú)有的對象(箭頭函數(shù)沒有)
// 該對象是一個偽數(shù)組扯罐,有 `length` 屬性且可以通過下標(biāo)訪問元素
// 該對象中的 `callee` 屬性代表函數(shù)本身
// `caller` 屬性代表函數(shù)的調(diào)用者

對于作用域鏈讼积,可以把它理解成包含自身變量對象和上級變量對象的列表肥照,通過 [[Scope]] 屬性查找上級變量

fooContext.[[Scope]] = [
  globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
  fooContext.VO,
  globalContext.VO
]

接下來讓我們看一個老生常談的例子,var

b()    // call b
console.log(a)    // undefined

var a = 'Hello world'

function b() {
  console.log('call b')
}

想必以上的輸出大家肯定都已經(jīng)明白了勤众,這是因?yàn)楹瘮?shù)和變量提升的原因建峭。通常提升的解釋是說將聲明的代碼移到了頂部,這其實(shí)沒有什么錯誤决摧,便于大家理解亿蒸。但是更準(zhǔn)確的解釋應(yīng)該是:在生成執(zhí)行上下文時,會有兩個階段掌桩。第一個階段是創(chuàng)建的階段(具體步驟是創(chuàng)建VO)边锁,JS 解釋器會找到需要提升的變量和函數(shù),并且給它們提前在內(nèi)存中開辟好空間波岛,函數(shù)的話會將整個函數(shù)存入內(nèi)存中茅坛,變量只聲明并且賦值為 undefined,所以在第二個階段,也就是代碼執(zhí)行階段贡蓖,我們可以直接提前使用曹鸠。
在提升的過程中,相同的函數(shù)會覆蓋上一個函數(shù)斥铺,并且函數(shù)優(yōu)先于變量提升

b()    // call b second

function b() {
  console.log('call b first')
}

function b() {
  console.log('call b second')
}

var b = 'Hello world'

var 會產(chǎn)生很多錯誤彻桃,所以在ES6中引入了 let。let 不能在聲明前使用晾蜘,但是這并不是常說的 let 不會提示邻眷,let 提升了聲明但沒有賦值,因?yàn)榕R時死區(qū)導(dǎo)致了并不能在聲明前使用剔交。
對于非匿名的立即執(zhí)行函數(shù)需要注意以下一點(diǎn)

var foo = 1
(function foo() {
  foo = 10
  console.log(foo)
}())    // -> ? foo() { foo = 10 ; console.log(foo) }

因?yàn)楫?dāng) JS 解釋器在遇到非匿名的立即執(zhí)行函數(shù)時肆饶,會創(chuàng)建一個輔助的特定對象,然后將函數(shù)名稱作為這個對象的屬性岖常,因此函數(shù)內(nèi)部才可以訪問到 foo驯镊,但是這個值又是只讀的,所以對它的賦值并不生效竭鞍,所以打印的結(jié)果還是這個函數(shù)阿宅,并且外部的值也沒有發(fā)生更改。

specialObject = {};

Scope = specialObject + Scope;

foo = new FuncionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo;    // {DontDelete}, {ReadOnly}

delete Scope[0];    // remove specialObject from the front of scope chain

閉包



閉包的定義很簡單:函數(shù) A 返回了一個函數(shù) B笼蛛,并且函數(shù) B 中使用了函數(shù) A 的變量洒放,函數(shù) B 就被稱為閉包。

function A() {
  let a = 1
  function B() {
    console.log(a)
  }
  return B
}

經(jīng)典面試題滨砍,循環(huán)中使用閉包解決 var 定義函數(shù)的問題

for (var i = 0; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}

首先因?yàn)?setTimeout 是個異步函數(shù)往湿,所以會先把循環(huán)全部執(zhí)行完畢,這時候 i 就是 6 了惋戏,所以會輸出一堆 6领追。

解決辦法兩種,第一種使用閉包

for (var i = 0; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

第二種就是使用 setTimeout 的第三個參數(shù)

for (var i = 0; i <= 5; i++) {
  setTimeout(function timer(j) {
    console.log(j)
  }, i * 1000, i)
}

第三種就是使用 let 定義 i 了

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

因?yàn)閷τ?let 來說响逢,它會創(chuàng)建一個塊級作用域绒窑,相當(dāng)于

{  // 形成塊級作用域
  let i = 0
  {
    let i1 = i
    setTimeout(function timer() {
      console.log(i1)
    }, i*1000)
  }
  i++
  {
    let i1 = i
  }
  i++
  {
    let i1 = i
  }
  ...
}

深淺拷貝


let a = {
  age: 1
}
let b = a
a.age = 2
console.log(b.age)    // 2

從上述例子中我們可以發(fā)現(xiàn),如果給一個變量賦值一個對象舔亭,那么兩者的值會是同一個引用些膨,其中一方改變,另一方也會相應(yīng)改變钦铺。
通常在開發(fā)中我們不希望出現(xiàn)這樣的問題订雾,我們可以使用淺拷貝來解決這個問題。

淺拷貝

首先可以通過 Object.assign 來解決這個問題

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age)    // 1

當(dāng)然我們也可以通過展開運(yùn)算符(...)來解決

let a = {
  age: 1
}
let b = {...a}
a.age = 2
console.log(b.age)  // 1

淺拷貝只解決了第一層的問題矛洞,如果接下去的值中還有對象的話洼哎,那么就又回到剛開始的話題了,兩者享有相同的引用。要解決這個問題噩峦,我們需要引入深拷貝锭沟。

深拷貝

這個問題通常可以通過 JSON.parse(JSON.stringify(object)) 來解決识补。

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first)  // FE

但是該方法也是有局限性的:

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函數(shù)
  • 不能解決循環(huán)引用的對象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果有這么一個循環(huán)引用對象族淮,你會發(fā)現(xiàn)你不能通過該方法深拷貝



在遇到函數(shù)、undefined 或者 symbol 的時候李请,該對象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你會發(fā)現(xiàn)在上述情況中,該方法會忽略掉函數(shù)和 undefined厉熟。
但是在通常情況下导盅,復(fù)雜數(shù)據(jù)都是可以序列化的,所以這個函數(shù)可以解決大部分問題揍瑟,并且該函數(shù)是內(nèi)置函數(shù)中處理深拷貝性能最快的白翻。當(dāng)然如果你的數(shù)據(jù)中含有以上三種情況下,可以使用 loadash 的深拷貝函數(shù)绢片。
如果你所需拷貝的對象含有內(nèi)置類型并且不包含函數(shù)滤馍,可以使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 注意該方法是異步的
// 可以處理 undefined 和循環(huán)引用對象
(async () => {
  const clone = await structuralClone(obj)
})()

模塊化



在有 Babel 的情況下,我們可以直接使用 ES6 的模塊化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJs 是 Node 獨(dú)有的規(guī)范底循,瀏覽器中使用就需要用到 Browserify 解析了巢株。

// a.js
module.exports = {
  a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a  // -> log 1

在上述代碼中,module.exports 和 exports 很容易混淆熙涤,讓我們來看看大致內(nèi)容實(shí)現(xiàn)

var module = require('./a.js')
module.a
// 這里其實(shí)就是包裝了一層立即執(zhí)行函數(shù)阁苞,這樣就不會污染全局變量了,重要的是 module 這里祠挫,module 是 Node 獨(dú)有的一個變量
module.exports = {
  a: 1
}
// 基本實(shí)現(xiàn)
var module = {
  exports: {}  // exports 就是個空對象
}
// 這個是為什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
  // 導(dǎo)出的東西
  var a = 1
  module.exports = a
  return module.exports
}

再來說說 module.exports 和 exports那槽,用法其實(shí)是相似的,但是不能對 exports 直接賦值等舔,不會有任何效果骚灸。
對于 CommonJS 和 ES6 中的模塊化的兩者區(qū)別是:

  • 前者支持動態(tài)導(dǎo)入,也就是 require(${path}/xx.js)慌植,后者目前不支持甚牲,但是已有提案
  • 前者是同步導(dǎo)入,因?yàn)橛糜诜?wù)端蝶柿,文件都在本地鳖藕,同步導(dǎo)入即使卡住主線程影響也不大。而后者是異步導(dǎo)入只锭,因?yàn)橛糜跒g覽器著恩,需要下載文件,如果也采用同步導(dǎo)入會對渲染有很大影響
  • 前者在導(dǎo)出時都是支拷貝,就算導(dǎo)出的值變了喉誊,導(dǎo)入的值也不會改變邀摆,所以如果想更新值,必須重新導(dǎo)入一次伍茄。但是后者采用實(shí)時綁定的方式栋盹,導(dǎo)入導(dǎo)出的值都指向同一個內(nèi)存地址,所以導(dǎo)入值會隨導(dǎo)出值變化
  • 后者會編譯成 require/exports 來執(zhí)行

AMD

AMD 是由 RequireJS 提出的

define(['./a', './b'], function(a, b) {
  a.do()
  b.do()
})
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  var b = require('./b')
  b.doSomething()
})

防抖



防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用敷矫。區(qū)別在于例获,假設(shè)一個用戶一直觸發(fā)這個函數(shù),且每次觸發(fā)函數(shù)的間隔小于 wait曹仗,防抖的情況下只會調(diào)用一次榨汤,而節(jié)流的情況會每隔一定的時間(參數(shù) wait)調(diào)用函數(shù)。
我們先來看一個袖珍版的防抖理解以下防抖的實(shí)現(xiàn):

// func 是用戶傳入需要防抖的函數(shù)
// wait 是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器 id
  let timer = 0
  // 這里返回的函數(shù)是每次用戶實(shí)際調(diào)用的防抖函數(shù)
  // 如果已經(jīng)設(shè)定過定時器了就清空上一次的定時器
  // 開始一個新的定時器怎茫,延遲執(zhí)行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出如果用戶調(diào)用該函數(shù)的間隔小于 wait 的情況下收壕,上一次的時間還未到就被清除了,并不會執(zhí)行函數(shù)

這是一個簡單版的防抖轨蛤,但是有缺陷蜜宪,這個防抖只能在最后調(diào)用。一般的防抖會有 immediate 選項(xiàng)祥山,表示是否立即調(diào)用圃验。這兩者的區(qū)別,舉個例子來說:

  • 例如在搜索引擎問題的時候缝呕,我們當(dāng)然是希望用戶輸入完最后一個字才調(diào)用查詢接口损谦,這個時候適用延遲執(zhí)行的防抖函數(shù),它總是在一連串(間隔小于 wait的)函數(shù)觸發(fā)之后調(diào)用岳颇。
  • 例如用戶給 interviewMap 點(diǎn) star 的時候照捡,我們希望用戶點(diǎn)第一下的時候就去調(diào)用接口,并且成功之后改變 star 按鈕的樣子话侧,用戶就可以立馬得到反饋是否 star 成功了栗精,這個情況適用立即執(zhí)行的防抖函數(shù),它總是在第一次調(diào)用瞻鹏,并且下次調(diào)用必須與前一次調(diào)用的時間間隔大于 wait 才會觸發(fā)悲立。

下面我們來實(shí)現(xiàn)一個帶有立即執(zhí)行選項(xiàng)的防抖函數(shù)

// 這里是用來獲取當(dāng)前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數(shù),返回函數(shù)連續(xù)調(diào)用時新博,空閑時間必須大于或等于 wait薪夕,func 才會執(zhí)行
 *
 * @param  {function} func        回調(diào)函數(shù)
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設(shè)置為ture時,是否立即調(diào)用函數(shù)
 * @return {function}             返回客戶調(diào)用函數(shù)
 */
function debounce(func, wait = 50, immediate = true) {
  let timer, context, args

  // 延遲執(zhí)行函數(shù)
  const later = () => setTimeout(() => {
    // 延遲執(zhí)行完畢赫悄,清空緩存的定時器序號
    timer = null
    // 延遲執(zhí)行的情況下原献,函數(shù)會在延遲函數(shù)種中執(zhí)行
    // 使用到之前緩存的參數(shù)和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這里返回的函數(shù)是每次實(shí)際調(diào)用的函數(shù)
  return function(...params) {
    // 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later)馏慨,就會創(chuàng)建一個
    if (!timer) {
      timer = later()
      // 如果是立即執(zhí)行,調(diào)用函數(shù)
      // 否則緩存參數(shù)和調(diào)用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
      // 如果已有延遲執(zhí)行函數(shù)(later)姑隅,調(diào)用的時候清除原來的并重新設(shè)定一個
      // 這樣做延遲函數(shù)會重新計(jì)時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

整體函數(shù)實(shí)現(xiàn)的不難写隶,總結(jié):

  • 對于按鈕防點(diǎn)擊來說的實(shí)現(xiàn):如果函數(shù)是立即執(zhí)行的,就立即調(diào)用讲仰,如果函數(shù)是延遲執(zhí)行的慕趴,就緩存上下文和參數(shù),放在延遲函數(shù)中去執(zhí)行鄙陡。一旦開始一個定時器冕房,只要定時器還在,每次點(diǎn)擊都重新計(jì)時趁矾。一旦定時器時間到了耙册,定時器重置為 null,就可以再次點(diǎn)擊了愈魏。
  • 對于延遲執(zhí)行函數(shù)來說的實(shí)現(xiàn):清除定時器 ID觅玻,如果是延遲調(diào)用就調(diào)用函數(shù)想际。

節(jié)流



防抖動和節(jié)流本質(zhì)是不一樣的培漏。防抖動是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行,節(jié)流是將多次執(zhí)行變成每隔一段時間執(zhí)行胡本。

/**
 * underscore 節(jié)流函數(shù)牌柄,返回函數(shù)連續(xù)調(diào)用時,func 執(zhí)行頻率限定為 次 / wait
 *
 * @param  {function}   func      回調(diào)函數(shù)
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   如果想忽略開始函數(shù)的的調(diào)用侧甫,傳入{leading: false}珊佣。
 *                                如果想忽略結(jié)尾函數(shù)的調(diào)用,傳入{trailing: false}
 *                                兩者不能共存披粟,否則函數(shù)不能執(zhí)行
 * @return {function}             返回客戶調(diào)用函數(shù)
 */
_.throttle = function(func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 之前的時間戳
  var previous = 0;
  // 如果 options 沒傳則設(shè)為空對象
  if (!options) options = {};
  // 定時器回調(diào)函數(shù)
  var later = function() {
    // 如果設(shè)置了 leading咒锻,就將 previous 設(shè)為 0
    // 用于下面函數(shù)的第一個 if 判斷
    previous = options.leading === false ? 0 : _.now();
    // 置空一是為了防止內(nèi)存泄漏,二是為了下面的定時器判斷
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    // 獲取當(dāng)前時間戳
    var now = _.now();
    // 首次進(jìn)入前者肯定為 true
    // 如果需要第一次不執(zhí)行函數(shù)
    // 就將上次時間戳設(shè)為當(dāng)前的
    // 這樣在接下來計(jì)算 remaining 的值時會大于 0
    if (!previous && options.leading === false) previous = now;
    // 計(jì)算剩余時間
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 如果當(dāng)前調(diào)用已經(jīng)大于上次調(diào)用時間 + wait
    // 或者用戶手動調(diào)了時間
    // 如果設(shè)置了 trailing守屉,只會進(jìn)入這個條件
    // 如果沒有設(shè)置 leading惑艇,那么第一次會進(jìn)入這個條件
    // 還有一點(diǎn),你可能會覺得開啟了定時器那么應(yīng)該不會進(jìn)入這個 if 條件了
    // 其實(shí)還是會進(jìn)入的拇泛,因?yàn)槎〞r器的延時
    // 并不是準(zhǔn)確的時間滨巴,很可能你設(shè)置了2秒
    // 但是他需要2.2秒才觸發(fā),這時候就會進(jìn)入這個條件
    if (remaining <= 0 || remaining > wait) {
        // 如果存在定時器就清理掉否則會調(diào)用二次回調(diào)
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
        // 判斷是否設(shè)置了定時器和 trailing
        // 沒有的話就開啟一個定時器
        // 并且不能不能同時設(shè)置 leading 和 trailing
        timeout = setTimeout(later, remaining);
    }
    return result;
  }
}

繼承



在 ES5 中俺叭,我們可以使用如下方式解決繼承的問題

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

以上繼承實(shí)現(xiàn)思路就是將子類的原型設(shè)置為父類的原型
在 ES6 中恭取,我們可以通過 class 語法輕松解決這個問題

class MyDate extends Date {
  test() {
    return this.getTime()
  }
}
let myDate = new MyDate()
myDate.test()

但是 ES6 不是所有瀏覽器都兼容,所以我們需要使用 Babel 來編譯這段代碼
如果你使用編譯過的代碼調(diào)用 myDate.test() 你就會發(fā)現(xiàn)出現(xiàn)了報(bào)錯

效果圖.png

因?yàn)樵?JS 底層有限制熄守,如果不是由 Date 構(gòu)造出來的實(shí)例的話蜈垮,是不能調(diào)用 Date 里的函數(shù)的耗跛。所以這也側(cè)面的說明了:ES6 中的 class 繼承與 ES5 中的一般繼承寫法是不同的。
既然底層限制了實(shí)例必須由 Date 構(gòu)造出來窃款,那么我們可以改變下思路實(shí)現(xiàn)繼承

function MyData() {

}
MyData.prototype.test = function() {
  return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)

以上繼承實(shí)現(xiàn)思路:先創(chuàng)建父類實(shí)例 => 改變實(shí)例原先的 proto 轉(zhuǎn)而連接到子類的 prototype => 子類的 prototype 的 proto 改為父類的 prototype课兄。
通過以上方法實(shí)現(xiàn)的繼承就可以完美解決 JS 底層的這個限制。

call晨继,apply烟阐,bind 區(qū)別



首先說下前兩者的區(qū)別。
call 和 apply 都是為了解決改變 this 指向紊扬。作用都是相同的蜒茄,只是傳參的方式不同。
除了第一個參數(shù)外餐屎,call 可以接收一個參數(shù)列表檀葛,apply 只接收一個參數(shù)數(shù)組。

let a = {
  value: 1
}
function getValue(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

模擬實(shí)現(xiàn) call 和 apply

可以從一下幾點(diǎn)來考慮如何實(shí)現(xiàn)

  • 不傳入第一個參數(shù)腹缩,那么默認(rèn)為 window
  • 改變了 this 的指向屿聋,讓新的對象可以執(zhí)行該函數(shù)。那么思路是否可以變成給新的對象添加一個函數(shù)藏鹊,然后在執(zhí)行完以后刪除润讥?
Function.prototype.myCall = function (context) {
  var context = context || window
  // 給 context 添加一個屬性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 將 context 后面的參數(shù)取出來
  var args = [...arguments].slice(1)
  var result = context.fn(...args)
  // 刪除 fn
  delete context.fn
  return result
}

以上就是 call 的思路,apply 的實(shí)現(xiàn)類似

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 需要判斷是否存儲第二個參數(shù)
  // 如果存在盘寡,就將第二個參數(shù)展開
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}

bind 和其他兩個方法作用也是一致的楚殿,只是該方法會返回一個函數(shù),并且我們可以通過 bind 實(shí)現(xiàn)柯里化竿痰。
同樣的脆粥,也來模擬實(shí)現(xiàn)下 bind

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一個函數(shù)
  return function F() {
    // 因?yàn)榉祷亓艘粋€函數(shù),我們可以 new F()影涉,所以需要判斷
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

Promise 實(shí)現(xiàn)



Promise 是 ES6 新增的語法变隔,解決了回調(diào)地獄的問題。
可以把 Promise 看出一個狀態(tài)機(jī)蟹倾。初始是 pending 狀態(tài)匣缘,可以通過函數(shù) resolve 和 reject,將狀態(tài)轉(zhuǎn)變?yōu)?resolved 或者 rejected 狀態(tài)喊式,狀態(tài)一旦改變就不能再次變化孵户。
then 函數(shù)會返回一個 Promise 實(shí)例,并且該返回值是一個新的實(shí)例而不是之前的實(shí)例岔留。因?yàn)?Promise 規(guī)范規(guī)定除了 pending 狀態(tài)夏哭,其他狀態(tài)是不可以改變的,如果返回的是一個相同實(shí)例的話献联,多個 then 調(diào)用就失去意義了竖配。
對于 then 來說何址,本質(zhì)上可以把它看成是 flatMap

// 三種狀態(tài)
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一個函數(shù)參數(shù),該函數(shù)會立即執(zhí)行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回調(diào)进胯,只有當(dāng) promise
  // 狀態(tài)為 pending 時才會緩存用爪,并且每個實(shí)例至多緩存一個
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是個 Promise,遞歸執(zhí)行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 異步執(zhí)行胁镐,保證執(zhí)行順序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 異步執(zhí)行偎血,保證執(zhí)行順序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解決以下問題
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 規(guī)范 2.2.7,then 必須返回一個新的 promise
  var promise2;
  // 規(guī)范 2.2.onResolved 和 onRejected 都為可選參數(shù)
  // 如果類型不是函數(shù)需要忽略盯漂,同時也實(shí)現(xiàn)了透傳
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 規(guī)范 2.2.4颇玷,保證 onFulfilled,onRjected 異步執(zhí)行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 異步執(zhí)行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考慮到可能會有報(bào)錯就缆,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 規(guī)范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 規(guī)范 2.3.1帖渠,x 不能和 promise2 相同,避免循環(huán)引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 規(guī)范 2.3.2
  // 如果 x 為 Promise竭宰,狀態(tài)為 pending 需要繼續(xù)等待否則執(zhí)行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次調(diào)用該函數(shù)是為了確認(rèn) x resolve 的
        // 參數(shù)是什么類型空郊,如果是基本類型就再次 resolve
        // 把值傳給下個 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 規(guī)范 2.3.3.3.3
  // reject 或者 resolve 其中一個執(zhí)行過得話,忽略其他的
  let called = false;
  // 規(guī)范 2.3.3切揭,判斷 x 是否為對象或者函數(shù)
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 規(guī)范 2.3.3.2狞甚,如果不能取出 then,就 reject
    try {
      // 規(guī)范 2.3.3.1
      let then = x.then;
      // 如果 then 是函數(shù)伴箩,調(diào)用 x.then
      if (typeof then === "function") {
        // 規(guī)范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 規(guī)范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 規(guī)范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 規(guī)范 2.3.4入愧,x 為基本類型
    resolve(x);
  }
}

以上就是根據(jù) Promise / A+ 規(guī)范來實(shí)現(xiàn)的代碼鄙漏,可以通過 promises-aplus-tests 的完整測試嗤谚。

Generator 實(shí)現(xiàn)



Generator 是 ES6 中新增的語法,和 Promise 一樣怔蚌,都可以用來異步編程

// 使用 * 表示這是一個 Generator 函數(shù)
// 內(nèi)部可以通過 yield 暫停代碼
// 通過調(diào)用 next 恢復(fù)執(zhí)行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

從以上代碼可以發(fā)現(xiàn)巩步,加上 * 的函數(shù)執(zhí)行后擁有了 next 函數(shù),也就是說函數(shù)執(zhí)行后返回了一個對象桦踊。每次調(diào)用 next 函數(shù)可以繼續(xù)執(zhí)行被暫停的代碼椅野。以下是 Generator 函數(shù)的簡單實(shí)現(xiàn)

// cb 也就是編譯過的 test 函數(shù)
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 編譯后可以發(fā)現(xiàn) test 函數(shù)變成了這樣
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以發(fā)現(xiàn)通過 yield 將代碼分割成幾塊
        // 每次執(zhí)行 next 函數(shù)就執(zhí)行一塊代碼
        // 并且表明下次需要執(zhí)行哪塊代碼
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
        // 執(zhí)行完畢
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

map、flatMap 和 reduce



Map 作用是生成一個新數(shù)組籍胯,遍歷原數(shù)組竟闪,將每個元素拿出來做一些變換然后 append 到新數(shù)組中。

[1, 2, 3].map((v) => v + 1)
// [2, 3, 4]

map 有三個參數(shù)杖狼,分別是當(dāng)前索引元素炼蛤、索引、原數(shù)組

['1', '2', '3'].map(parseInt)
// parseInt('1', 0) -> 1
// parseInt('2', 1) -> NaN
// parseInt('3', 2) -> NaN

flatMap 和 map 的作用幾乎是相同的蝶涩,但是對于多維數(shù)組來說理朋,會將原數(shù)組降維絮识,可以將 flatMap 看成是 map + flatten,目前該函數(shù)在瀏覽器中還不支持嗽上。

[1, [2], 3].map((v) => v + 1)
// [2, 3, 4]

如果將想將一個多維數(shù)組徹底的降維次舌,可以這樣實(shí)現(xiàn)

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce((a, b) => [...a, ...flattenDeep(b)], [])
  : [arr])

reduce 作用是數(shù)組中的值組合起來,最終得到一個值

function a(){
  console.log(1)
}
function b(){
  console.log(2)
}
[a, b].reduce((a, b) => a(b()))
// -> 2 1

async 和 await



一個函數(shù)如果加上 async兽愤,那么該函數(shù)就會返回一個 Promise

async function test(){
  return '1'
}
console.log(test());
// Promise {<resolved>: '1'}

可以把 async 看成函數(shù)返回值使用 Promise.resolve() 包裹了下彼念。
await 只能在 async 函數(shù)中使用

function sleep(){
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('finish')
      resolve('sleep')
    }, 2000)
  })
}
async function test(){
  let value = await sleep()
  console.log('object')
}
test()

上面代碼會先打印 finish,然后再打印 object浅萧。因?yàn)?await 會等待 sleep 函數(shù) resolve国拇,所以即使后面是同步代碼,也不會先去執(zhí)行同步代碼再來執(zhí)行異步代碼惯殊。
async 和 await 相比直接使用 Promise 來說酱吝,優(yōu)勢在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼土思。缺點(diǎn)在于濫用 await 可能會導(dǎo)致性能問題务热,因?yàn)?await 會阻塞代碼,也許之后的異步代碼并不依賴于前者己儒,但仍然需要等待前者完成崎岂,導(dǎo)致代碼失去了并發(fā)性。
下面來看一個使用 await 的代碼

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a)
  a = (await 10) + a
  console.log('3', a)
}
b()
a++
console.log('1', a)
  • 首先函數(shù) b 先執(zhí)行闪湾,在執(zhí)行到 await 10 之前變量 a 還是 0冲甘,因?yàn)樵?await 內(nèi)部實(shí)現(xiàn)了 generators,generators 會保留堆棧中的東西途样,所以這時候 a = 0 被保存了下來江醇。
  • 因?yàn)?await 是異步操作,遇到 await 就會立即返回一個 pending 狀態(tài)的 Promise 對象何暇,暫時返回執(zhí)行代碼的 控制權(quán)陶夜,使得函數(shù)外的代碼得以繼續(xù)執(zhí)行,所以會先執(zhí)行 console.log('1', a)裆站。
  • 這時候同步代碼執(zhí)行完畢条辟,開始執(zhí)行異步代碼,將保存下來的值拿出來使用宏胯,這時候 a = 10羽嫡。
  • 然后后面就是常規(guī)執(zhí)行代碼了。

轉(zhuǎn)載于 https://yuchengkai.cn/docs/frontend/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肩袍,一起剝皮案震驚了整個濱河市杭棵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌了牛,老刑警劉巖颜屠,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辰妙,死亡現(xiàn)場離奇詭異,居然都是意外死亡甫窟,警方通過查閱死者的電腦和手機(jī)密浑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粗井,“玉大人尔破,你說我怎么就攤上這事〗匠模” “怎么了懒构?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耘擂。 經(jīng)常有香客問我胆剧,道長,這世上最難降的妖魔是什么醉冤? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任秩霍,我火速辦了婚禮,結(jié)果婚禮上蚁阳,老公的妹妹穿的比我還像新娘铃绒。我一直安慰自己,他們只是感情好螺捐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布颠悬。 她就那樣靜靜地躺著,像睡著了一般定血。 火紅的嫁衣襯著肌膚如雪赔癌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天糠悼,我揣著相機(jī)與錄音届榄,去河邊找鬼浅乔。 笑死倔喂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的靖苇。 我是一名探鬼主播席噩,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贤壁!你這毒婦竟也來了悼枢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤脾拆,失蹤者是張志新(化名)和其女友劉穎馒索,沒想到半個月后莹妒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绰上,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年旨怠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜈块。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡鉴腻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出百揭,到底是詐尸還是另有隱情爽哎,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布器一,位于F島的核電站课锌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏祈秕。R本人自食惡果不足惜产镐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望踢步。 院中可真熱鬧癣亚,春花似錦、人聲如沸获印。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兼丰。三九已至玻孟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鳍征,已是汗流浹背黍翎。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艳丛,地道東北人匣掸。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像氮双,于是被迫代替她去往敵國和親碰酝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348